Gas Station (TypeScript)
Overview
In this tutorial you'll learn how to sponsor transactions with Shinami's Gas Station. For more on the benefits of sponsoring transactions and to see an image of an end-to-end sponsorship flow, see our Gas Station high-level guide The full code for this TypeScript tutorial is available on GitHub.
Gas Station requests must be from your BE
Our Gas Station does not support CORS, so if you attempt to make requests to it from the FE you'll get a CORS error. We do this because exposing a Gas Station API key on the FE is a security risk - an attacker could drain the APT in your Gas Station fund associated with the key by using it to sponsor transactions.
For an overview of ways to integration frontend signing with backend sponsorship, see our Appendix. If you're using our Invisible Wallets, you can simply build a transaction on your backend and then sign, sponsor, and submit it with one wal_executeGaslessTransaction
request .
Understanding errors
Check the error code and message of any errors you get. We outline common errors in our Error Reference - make sure to check out the section specific to Aptos Gas Station as well as the "General Error Codes" section at the top that apply to all Shinami services.
Required setup
Note: this tutorial requires a Shinami account. If you do not have one, you can sign up for the waitlist here - use referral code "Aptos Gas Station".
1. Create a Shinami Gas Station fund on Testnet and deposit SUI
You'll need a Testnet Gas Station fund with APT in it in order to sponsor transactions. See guidance on creating a fund and adding APT in our FAQ.
2. Clone the github repo and install dependencies
Clone the shinami-examples github repo, cd into the shinami-examples/aptos/typescript/backend_examples
directory, and run npm install
to install the dependencies for running the code. If you need to install npm and Node.js, see here . Below, we'll be using the gas_station.ts
file in the shinami-examples/aptos/typescript/backend_examples/src
directory.
3: Create an API access key and copy it into the file.
Create a Gas Station API access key
You use API access keys to interact with Shinami's services. For this tutorial, we'll create one access key that has Gas Station
rights for Testnet. See our Authentication and API Keys guide for info on how to set up a key and link it to the fund you created in step 1.
Add it to the gas_station.ts file
Once you have your key, use it as the value for the SHINAMI_TESTNET_GAS_KEY
constant. You'll see that we immediately use it to instantiate a Gas Station client (for sponsoring transactions).
// Create a Shinami Gas Station client for sponsoring our transactions.
const SHINAMI_TESTNET_GAS_KEY = "{{APTOS_TESTNET_GAS_STATION_ACCESS_KEY}}";
const gasStationClient = new GasStationClient(SHINAMI_TESTNET_GAS_KEY);
4: Open your Shinami dashboard
Technically not required, but we recommend opening the Gas Station page of your Shinami dashboard and clicking on the "Completed transactions" tab. After running the code examples, take a look at this page. It may take a moment, but you'll see the digests of transactions you sponsor that are committed to the blockchain appear as in the image below. This tab, and the "In flight transactions" tab (for successful sponsorships that haven't yet been committed to the Aptos blockchain) can be helpful when testing.
![](https://files.readme.io/77eea5c-Screenshot_2024-05-23_at_2.04.28_PM.png)
Code examples
Overview
Below, we'll review each of our sample code functions and how to run them. At a high-level, you'll uncomment just one sample code function - e.g. sponsorTransactionSimple()
in the code block below. Then, save the change, run tsc
in the shinami-examples/aptos/typescript/backend_examples
directory to compile, and run node build/gas_station.ts
to run. This will work as downloaded for the simple transaction examples, but the multi-agent ones require compiling a Move script (explained in the Appendix).
//
// -- Choose which sample code function to use to generate a PendingTransactionResponse //
//
const committedTransaction = await
sponsorTransactionSimple();
// sponsorTransactionMultiAgent();
// sponsorAndSubmitSignedTransactionSimple();
// sponsorAndSubmitSignedTransactionMultiAgent();
// Wait for the transaction to move past the pending state
if (committedTransaction) {
const executedTransaction = await aptos.waitForTransaction({
transactionHash: committedTransaction.hash
});
console.log("\nTransaction hash:", executedTransaction.hash);
console.log("Transaction status:", executedTransaction.vm_status);
}
Each of the sample code functions returns a Promise<PendingTransactionResponse>
. If our function was successful, we wait for the transaction to be committed to the blockchain and then print out the hash and status. Example:
Transaction hash: 0x8952262bec100c8426f089fa3230eb582bbfd8e42e3cfef98dc8bea449927494
Transaction status: Executed successfully
You can look up the digest in an explorer like Aptos Explorer or Aptos Scan (make sure you've set the explorer to Testnet first). This is an image showing that my transaction had both a sender and a fee payer signature (your sender and feePayer addresses will be different):
![](https://files.readme.io/e566841-Screenshot_2024-05-20_at_8.16.48_PM.png)
Sponsor a simple transaction
Understand the code
The sponsorTransactionSimple
function performs all the steps needed to build, sponsor, sign, and submit a simple transaction:
- Create an Account to use as the sender (see how it's done in the Appendix).
- Build the transaction (see how it's done in the Appendix).
- Sponsor the transaction with a call to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the
console.log()
statement below. - Sign the transaction as the sender.
- Submit the transaction with the sender and feePayer signatures and returns the result.
// Build, sponsor, sign, and execute a simple Move call transaction
async function sponsorTransactionSimple(): Promise<PendingTransactionResponse> {
// 1. Set up our sender.
const sender = await generateSingleKeyAccountEd25519();
// 2. Build a simple transaction.
let transaction = await buildSimpleMoveCallTransaction(sender.accountAddress);
// 3. Sponsor the transaction with Shinami Gas Station.
let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction);
// Note that the SDK updates the transaction's feePayer address on a successful sponsorship
console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress);
// 4. Generate the sender's signature.
const senderAuthenticator = aptos.transaction.sign({
signer: sender,
transaction: transaction
});
// 5. Submit the transaction with the sender and fee payer signatures
return await aptos.transaction.submit.simple({
transaction,
senderAuthenticator,
feePayerAuthenticator: feePayerAuthenticator,
});
}
Update, save, compile, run
Make sure the function is the only sample code function uncommented:
//
// -- Choose which sample code function to use to generate a PendingTransactionResponse //
//
const committedTransaction = await
sponsorTransactionSimple();
// sponsorTransactionMultiAgent();
// sponsorAndSubmitSignedTransactionSimple();
// sponsorAndSubmitSignedTransactionMultiAgent();
Save the file if you made a change. Run tsc
in the shinami-examples/aptos/typescript/backend_examples
directory to compile. Then, run node build/gas_station.js
. If successful, you can view the sponsorship on the "Completed transactions" tab of the Aptos Gas Station page of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like Aptos Explorer (make sure you've set the explorer to Testnet)
Sponsor a multi-agent transaction
Understand the code
The sponsorTransactionMultiAgent
function performs all the steps needed to build, sponsor, sign, and submit a multi-agent Move script transaction:
- Generate two, funded Accounts that will swap Octa (see how it's done in the Appendix).
- Build the transaction (see how it's done in the Appendix).
- Sponsor the transaction with a request to Shinami Gas Station. Note that our TypeScript SDK updates the feePayer address upon a successful sponsorship as shown by the
console.log()
statement below. - Generate the sender and secondary signer signatures.
- Submit the transaction with the sender, secondary signer, and feePayer signatures and returns the result.
// Build, sponsor, sign, and execute a multiAgent Move script transaction
async function sponsorTransactionMultiAgent(): Promise<PendingTransactionResponse> {
// 1. Generate two funded accounts to act as sender and secondary signer
const sender = await generateSingleKeyAccountEd25519(true);
const secondarySigner = await generateSingleKeyAccountEd25519(true);
// 2. Build a multiAgent transaction
let transaction = await buildMultiAgentScriptTransaction(sender.accountAddress, secondarySigner.accountAddress);
// 3. Sponsor the transaction with Shinami Gas Station
let feePayerAuthenticator = await gasStationClient.sponsorTransaction(transaction);
// Note that the SDK updates the transaction's feePayer address on a successful sponsorship
console.log("\ntransaction.feePayerAddress post-sponsorship:", transaction.feePayerAddress);
// 4. Generate the sender and secondary signer signatures
const senderAuthenticator = aptos.transaction.sign({
signer: sender,
transaction
});
const secondarySignerAuthenticator = aptos.transaction.sign({
signer: secondarySigner,
transaction
});
// 5. Submit the transaction with the sender, seconardy signer, and feePayer signatures
return await aptos.transaction.submit.multiAgent({
transaction,
senderAuthenticator,
additionalSignersAuthenticators: [secondarySignerAuthenticator],
feePayerAuthenticator: feePayerAuthenticator
});
}
Update, save, compile, run
In order to run this example, you'll need to compile the Move script used as shown in the Appendix.
Make sure the function is the only sample code function uncommented:
//
// -- Choose which sample code function to use to generate a PendingTransactionResponse //
//
const committedTransaction = await
// sponsorTransactionSimple();
sponsorTransactionMultiAgent();
// sponsorAndSubmitSignedTransactionSimple();
// sponsorAndSubmitSignedTransactionMultiAgent();
Save the file if you made a change. Run tsc
in the shinami-examples/aptos/typescript/backend_examples
directory to compile. Then, run node build/gas_station.js
. If successful, you can view the sponsorship on the "Completed transactions" tab of the Aptos Gas Station page of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like Aptos Explorer (make sure you've set the explorer to Testnet)
Sponsor and submit a signed, simple transaction
Understand the code
The sponsorAndSubmitSignedTransactionSimple
function performs all the steps needed to build and sign a simple transaction, and then send it to Gas Station for sponsorship and submission to the Aptos blockchain:
-
Create an Account to use as the sender (see how it's done in the Appendix).
-
Build the transaction (see how it's done in the Appendix).
-
Generate the sender's signature.
-
Make a request to Gas Station to sponsor and submit the signed transaction, returning the result.
// Build, sign, then sponsor and submit a simple transaction
async function sponsorAndSubmitSignedTransactionSimple(): Promise<PendingTransactionResponse> {
// 1. Set up our sender.
const sender = await generateSingleKeyAccountEd25519();
// 2. Build a simple transaction.
const transaction = await buildSimpleMoveCallTransaction(sender.accountAddress);
// 3. Generate the sender's signature.
const senderAuthenticator = aptos.transaction.sign({
signer: sender,
transaction
});
// 4. Ask Shinami to sponsor and submit the transaction
return await gasStationClient.sponsorAndSubmitSignedTransaction(
transaction,
senderAuthenticator
);
}
Update, save, compile, run
Make sure the function is the only sample code function uncommented:
//
// -- Choose which sample code function to use to generate a PendingTransactionResponse //
//
const committedTransaction = await
// sponsorTransactionSimple();
// sponsorTransactionMultiAgent();
sponsorAndSubmitSignedTransactionSimple();
// sponsorAndSubmitSignedTransactionMultiAgent();
Save the file if you made a change. Run tsc
in the shinami-examples/aptos/typescript/backend_examples
directory to compile. Then, run node build/gas_station.js
. If successful, you can view the sponsorship on the "Completed transactions" tab of the Aptos Gas Station page of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like Aptos Explorer (make sure you've set the explorer to Testnet)
Sponsor and submit a multi-agent transaction
Understand the code
The sponsorAndSubmitSignedTransactionMultiAgent
function performs all the steps needed to build and sign a multi-agent Move script transaction, and then send it to Gas Station for sponsorship and submission to the Aptos blockchain:
-
Generate two, funded Accounts that will swap Octa (see how it's done in the Appendix).
-
Build the transaction (see how it's done in the Appendix).
-
Generate the sender and secondary signer signatures.
-
Make a request to Gas Station to sponsor and submit the signed transaction, returning the result.
// Build, sign, then sponsor and submit a multiAgent transaction
async function sponsorAndSubmitSignedTransactionMultiAgent(): Promise<PendingTransactionResponse> {
// 1. Generate two funded accounts to act as sender and secondary signer
const sender = await generateSingleKeyAccountEd25519(true);
const secondarySigner = await generateSingleKeyAccountEd25519(true);
// 2. Build a multiAgent transaction
let transaction = await buildMultiAgentScriptTransaction(sender.accountAddress, secondarySigner.accountAddress);
// 3. Generate the sender and secondary signer signatures
const senderAuthenticator = aptos.transaction.sign({
signer: sender,
transaction
});
const secondarySignerAuthenticator = aptos.transaction.sign({
signer: secondarySigner,
transaction
});
// 4. Ask Shinami to sponsor and submit the transaction
return await gasStationClient.sponsorAndSubmitSignedTransaction(
transaction,
senderAuthenticator,
[secondarySignerAuthenticator]
);
}
Update, save, compile, run
In order to run this example, you'll need to compile the Move script used as shown in the Appendix.
Make sure the function is the only sample code function uncommented:
//
// -- Choose which sample code function to use to generate a PendingTransactionResponse //
//
const committedTransaction = await
// sponsorTransactionSimple();
// sponsorTransactionMultiAgent();
// sponsorAndSubmitSignedTransactionSimple();
sponsorAndSubmitSignedTransactionMultiAgent();
Save the file if you made a change. Run tsc
in the shinami-examples/aptos/typescript/backend_examples
directory to compile. Then, run node build/gas_station.js
. If successful, you can view the sponsorship on the "Completed transactions" tab of the Aptos Gas Station page of your Shinami Dashboard. You can also look up the transaction digest printed to the console in an explorer like Aptos Explorer (make sure you've set the explorer to Testnet)
Appendix
Build a SimpleTransaction for sponsorship
The below function creates a transaction that calls a function on a Move module we've deployed to Testnet (it's from an Aptos tutorial). The set_message
function allows the caller to store a message at their address inside a MessageHolder
. If there was already a value the user was storing, the function emits an event that says what the messaged changed from and to, and who made the change.
The transaction's expiration timestamp must be set to a time within the next hour (as is required by our Gas Station). When you control the generation of the sender's signature, as with an embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it.
// Build a Move call simple transaction with a fee payer
async function buildSimpleMoveCallTransaction(sender: AccountAddress, expirationSeconds?: number): Promise<SimpleTransaction> {
let transaction = await aptos.transaction.build.simple({
sender: sender,
withFeePayer: true,
data: {
function: "0xc13c3641ba3fc36e6a62f56e5a4b8a1f651dc5d9dc280bd349d5e4d0266d0817::message::set_message",
functionArguments: [new MoveString("Test message")]
},
options: {
expireTimestamp: expirationSeconds
}
});
console.log("\nResponse from aptos.transaction.build.simple()");
console.log(transaction);
return transaction;
}
Build a MultiAgentTransaction with a compiled Move script
Step 1: Building a MultiAgentTransaction for sponsorship.
For a multi-agent transaction, you'll need to provide the secondary signers when building the transaction (in addition to the sender). The transaction's expiration timestamp must be set to a time within the next hour (as is required by our Gas Station). When you control the generation of the sender's signature, as with an embedded wallet you control for the user, you can likely use the SDK default, which is 20 seconds from now. When you need to wait on a signature you don't control, as with a connected wallet where the user must approve a signing message, you can explicitly set it.
This function reads a complied Move script inside your local directory and builds a MultiAgentTransaction with it. You'll need to compile the script to use this function (see "Step 2" below).
// Build a multi-agent script transaction with one secondary signer and a fee payer.
async function buildMultiAgentScriptTransaction(sender: AccountAddress, secondarySigner: AccountAddress,
expirationSeconds?: number) : Promise<MultiAgentTransaction> {
let buffer = readFileSync("./move/build/test/bytecode_scripts/unfair_swap_coins.mv");
let bytecode = Uint8Array.from(buffer);
let transaction = await aptos.transaction.build.multiAgent({
sender: sender,
secondarySignerAddresses: [secondarySigner],
withFeePayer: true,
data: {
bytecode: bytecode,
functionArguments: []
},
options: {
expireTimestamp: expirationSeconds
}
});
console.log("\nResponse from aptos.transaction.build.multiAgent()");
console.log(transaction);
return transaction;
}
Step 2: review and build our Move script
Move script
The script we'll use is located at shinami-examples/aptos/typescript/backend_examples/move/sources/unfair_swap_coins.move
script {
use aptos_framework::aptos_coin;
use aptos_framework::coin;
use aptos_framework::signer;
fun unfair_swap_coins(
sender: &signer,
secondary: &signer
) {
let coin_first = coin::withdraw<aptos_coin::AptosCoin>(sender, 100);
let coin_second = coin::withdraw<aptos_coin::AptosCoin>(secondary, 200);
coin::deposit(signer::address_of(secondary), coin_first);
coin::deposit(signer::address_of(sender), coin_second);
}
}
Build the Move script
For this, you'll need to install the Aptos CLI. Once you've done so, run cd move
to get to the root of the shinami-examples/aptos/typescript/backend_examples/move
directory. Then, run aptos move compile
. You should see a build/test/bytecode_scripts/unfair_swap_coins.mv
. That's the compiled, bytecode version of the below Move script.
This Move script has the sender give the secondary signer 100 Octa in exchange for 200 Octa - not fair! For more guidance on how to run a multi-agent transaction see here, and for guidance on running Move scripts see here.
Generating an account for testing
This is a helper function to generate Aptos accounts for our tutorial and fund them if needed (as in the sponsorTransactionBlockMultiAgent
function). Your app should determine the best way to manage any keys it controls. For the sole purpose of tutorial testing, if you want to reuse an account see the next Appendix section: Building a SingleKeyAccount from a private key.
async function generateSingleKeyAccountEd25519(fund = false) : Promise<SingleKeyAccount> {
const account: SingleKeyAccount = SingleKeyAccount.generate({ scheme: SigningSchemeInput.Ed25519});
if (fund) {
await aptos.fundAccount({
accountAddress: account.accountAddress,
amount: 100000000
});
}
return account;
}
Building a SingleKeyAccount from a private key
For the purposes of reusing the same account for repeated tests with this tutorial, you can find and reuse a SingleKeyAccount's private key a shown below. This is especially useful when using a funded account for initial testing as there are limits on using the Testnet faucet.
This is not a recommendation for production code, just a convenience for the tutorial. Your app should determine the best way to manage any keys it controls.
//
// First run of code - generate a funded SingleKeyAccount
// and print it's private key to the console (just for use with this tutorial)
//
const account = await generateSingleKeyAccountEd25519(true);
const PRIVATE_KEY = Buffer.from(account.privateKey.toUint8Array()).toString('hex');
console.log("\PRIVATE_KEY value:", PRIVATE_KEY);
const sender: SingleKeyAccount = new SingleKeyAccount({
privateKey: new Ed25519PrivateKey(PRIVATE_KEY)
});
// First run output example: "PRIVATE_KEY value: abcd1234"
//
// Subsequent runs of the code - comment out un-needed lines
// and use the value that was printed to the console.
//
//const account = await generateSingleKeyAccountEd25519(true);
const PRIVATE_KEY = "abcd1234";
//console.log("\PRIVATE_KEY value:", PRIVATE_KEY);
const sender: SingleKeyAccount = new SingleKeyAccount({
privateKey: new Ed25519PrivateKey(PRIVATE_KEY)
});
Sponsoring a transaction for a non-funded account
As our examples above show, you can sponsor a transaction for an Account that has not yet been funded. The transaction fee for an account's first transaction will cost more than the fee for the same transaction at the same time for a funded account. Example fee statements are below, but your results may vary.
Example 0x1::transaction_fee::FeeStatement
for a message::setmessage
Testnet Move call transaction from an unfunded account:
{
execution_gas_units:"4"
io_gas_units:"1"
storage_fee_octas:"92840"
storage_fee_refund_octas:"0"
total_charge_gas_units:"933"
}
Example 0x1::transaction_fee::FeeStatement
for a message::setmessage
Testnet Move call transaction from a funded account:
{
execution_gas_units:"3"
io_gas_units:"1"
storage_fee_octas:"43680"
storage_fee_refund_octas:"0"
total_charge_gas_units:"441"
}
Tips for setting your sponsorship budget
See the Aptos doc on Gas and Storage Fees for a more detailed overview of how transaction costs are determined, as well as guidance on how to estimate the costs for a transaction.
See the Aptos Gas Station tab on the Billing page of your dashboard to see how Shinami charges for sponsorships. Note: only workspace admins can view and change billing information.
Integrating FE signing with BE sponsorship
Four data flow options
Below are the four options for combining frontend signing with backend sponsorship.
Generate and submit the transaction on the FE
- FE: Generate the
AnyRawTransaction
. Serialize it and send to the BE. - BE: Deserialize the
AnyRawTransaction
and sponsor it with agas_sponsorTransaction
request to Shinami's Gas Station. Serialize the feePayerAccountAuthenticator
and the feePayerAccountAddress
you get back from the request and send them to the FE. - FE: Deserialize the feePayer
AccountAuthenticator
. Deserialize the feePayerAccountAddress
and set it as the transaction'sfeePayerAddress
. Obtain the sender signature (AccountAuthenticator
). Submit the updatedAnyRawTransaction
along with the sender and feePayerAccountAuthenticator
s.
Generate the transaction on the FE, submit it on the BE
- FE: Generate the
AnyRawTransaction
. Obtain the sender signature (AccountAuthenticator
). Serialize both and send them to the BE. - BE: Deserialize the
AnyRawTransaction
and the senderAccountAuthenticator
. Use them both in agas_sponsorAndSubmitSignedTransaction
request to Shinami's Gas Station. Return the result to the FE as needed.
Generate the transaction on the BE, submit it on the FE
- FE: Send any data from the FE to the BE needed to generate the transaction (e.g. the sender address, user input, etc.).
- BE: Generate the
AnyRawTransaction
and sponsor it with agas_sponsorTransaction
request to Shinami's Gas Station. Serialize the feePayerAccountAuthenticator
you get back from the request. Update the transaction's feePayerAddress (our TypeScript SDK does this automatically, or you can set it explicitly with the feePayer'sAccountAddress
returned bygas_sponsorTransaction
). Serialize the updatedAnyRawTransaction
and send it along with the serialized feePayerAccountAuthenticator
to the FE. - FE: Deserialize the feePayer
AccountAuthenticator
and theAnyRawTransaction
. Obtain the sender signature (AccountAuthenticator
). Submit theAnyRawTransaction
along with the sender and feePayerAccountAuthenticator
s.
Generate and submit the transaction on the BE
- FE: Send any data from the FE to the BE needed to generate the transaction (e.g. the sender address, user input, etc).
- BE: Generate the
AnyRawTransaction
and sponsor it with agas_sponsorTransaction
request to Shinami's Gas Station. Serialize the feePayerAccountAuthenticator
you get back from the request. Update the transaction's feePayerAddress (our TypeScript SDK does this automatically, or you can set it explicitly with the feePayer'sAccountAddress
returned bygas_sponsorTransaction
). Serialize the updatedAnyRawTransaction
and send it along with the serialized feePayerAccountAuthenticator
to the FE. - FE: Obtain the sender signature (
AccountAuthenticator
) and serialize it. Send the serialized feePayer and senderAccountAuthenticator
s and theAnyRawTransaction
to the BE. - BE: Deserialize the
AnyRawTransaction
and the sender and feePayerAccountAuthenticator
s. Submit them to the Aptos blockchain. Return the result to the FE as needed.
Passing data: serializing and deserializing
Here are examples of serializing and deserializing key data types you might pass between your frontend and backend. Use a separate Serializer and Deserializer for each piece of data you are working it. In the examples below, though, we don't use a Serializer.
Serialize on one side: BE or FE
import { SimpleTransaction, AccountAuthenticator, AccountAddress } from "@aptos-labs/ts-sdk";
const serializedAccountAuthenticator = authenticator.bcsToHex().toString();
const serializedAccountAddress = accountAddress.bcsToHex().toString();
const serializedSimpleTransaction = simpleTx.bcsToHex().toString();
Deserialize on the other side
import {
SimpleTransaction, Deserializer, AccountAuthenticator, Hex, AccountAddress
} from "@aptos-labs/ts-sdk";
AccountAuthenticator.deserialize(new Deserializer(
Hex.fromHexString(serializedAccountAuthenticator).toUint8Array()));
AccountAddress.deserialize(new Deserializer(
Hex.fromHexString(serializedAccountAddress).toUint8Array()));
SimpleTransaction.deserialize(new Deserializer(
Hex.fromHexString(serializedSimpleTransaction).toUint8Array()));
Updated about 8 hours ago