Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/build/guides/archival/create-restoration-footprint-js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ async function createRestorationFootprint(
readWrite: [contractDataXDR],
}),
instructions: 0,
readBytes: 0,
diskReadBytes: 0,
Comment thread
ElliotFriend marked this conversation as resolved.
writeBytes: 0,
}),
resourceFee: xdr.Int64.fromString("0"),
// @ts-ignore
ext: new xdr.ExtensionPoint(0),
ext: new xdr.SorobanTransactionDataExt(0),
});
// Restore transaction with created restoration footprint
const restoreTx: Transaction = new TransactionBuilder(account, { fee: fee })
Expand Down Expand Up @@ -107,10 +107,16 @@ Next up we are preparing the (Soroban) transaction data with:

- `resources`: Specifies the resources needed for the transaction.
- `footprint`: Defines which parts of the ledger will be read and written.
- `instructions`, `readBytes`, `writeBytes`: Sets the resource limits (all set to 0 here).
- `instructions`, `diskReadBytes`, `writeBytes`: Sets the resource limits (all set to 0 here).
- `resourceFee`: Sets the resource fee to 0 (placeholder).
- `ext`: Extension point for future use (set to 0).

Note that for restoration footprints we only need to fill in `readWrite`.

:::caution

The `diskReadBytes`, `writeBytes`, and `resourceFee` values above are zeroed **placeholders** to keep the example focused on footprint construction. A real restoration will not succeed with these left at `0` — Core compares the restored entry's serialized size against the declared resources and rejects the operation (`RESTORE_FOOTPRINT_RESOURCE_LIMIT_EXCEEDED`) when they are too low. Before signing, populate valid resource values by running the transaction through [simulation](../../../learn/fundamentals/contract-development/contract-interactions/transaction-simulation.mdx) (for example with `server.prepareTransaction`), which fills in the required resources and fee for you.

:::

The transaction can now be submitted to the Stellar (test) network & signed to restore the specified contract’s data.
283 changes: 215 additions & 68 deletions docs/build/guides/archival/extend-persistent-entry-js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,136 +3,283 @@ title: Extend a persistent entry and contract using the JavaScript SDK
description: How to extend a persistent entry and contract using the JavaScript SDK
---

Persistent storage provides a durable mechanism to retain data on the network over a long period. User data, contract instances and their corresponding code (wasm hash) are stored as instance storage with persistent durability to ensure that they remain available, and are managed efficiently by the network's archival system.
Persistent storage gives you a durable place to keep contract data on the network for a long time. Anything you can't cheaply recreate (user balances, configuration, and the like) belongs there. A contract's instance entry and the Wasm code entry it points to are separate ledger entries, but they live under the same archival rules, so they need the same care.

Each entry made to the persistent storage is stored as a key-value store. An entry consists of:
Every persistent entry on the ledger is a key-value pair made up of:

- A key (identifier for the data)
- A value (the data itself)
- A Time-To-Live (TTL) that determines how long the data remains on the network
- a key (the identifier for the data),
- a value (the data itself), and
- a [Time To Live](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#ttl) (TTL), which is how many ledgers remain until the entry is no longer live. (The absolute cutoff is stored on the entry itself as its [`liveUntilLedger`](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#live-until-ledger).)

When the TTL expires, the data is then archived on the network and is no longer accessible unless it is unarchived. To prevent data and contracts from being inaccessible, the TTL can be extended before it expires.
Once an entry's TTL runs out, the network archives it. Archived persistent data isn't lost (it can be [restored](./restore-data-js.mdx)), but nothing can read it until that happens. Keeping the TTL topped up before it expires is the cheaper, less exciting path, and that's what we'll walk through here.

## Extension Process
By the end of this guide you'll have four helper functions you can drop into a project: one that extends a single persistent entry, one for a contract's instance, one for the Wasm code that instance points to, and one that extends all three together in a single operation.

1. Create the Ledger key for the entry to be extended.
2. Build a transaction that includes the `extendFootprintTtl` operation with your target TTL and attach the Soroban transaction data.
3. Prepare the transaction, sign it with your secret key, and submit it to the network.
## The extension process

### Extending a persistent entry
1. Build the ledger key for the entry you want to extend.
2. Build a transaction containing an `extendFootprintTtl` operation with your target TTL, and attach the Soroban transaction data.
3. Prepare the transaction (this runs simulation to fill in the resources and fee for you), sign it with your secret key, and submit it to the network.

```javascript
const StellarSdk = require("@stellar/stellar-sdk");
const { Server } = require("@stellar/stellar-sdk/rpc");
:::note

async function extendPersistentEntryTTL(contractId, storageKey, sourceKeypair) {
`extendFootprintTtl` sets a _floor_, not an exact value. Per the [`ExtendFootprintTTLOp` semantics](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#extendfootprintttlop), the operation guarantees each entry in the read-only footprint lives for _at least_ `extendTo` ledgers from now. Entries that already live longer than your target are left alone, so a repeated extension is harmless.

There's also a ceiling: an entry can only be extended up to the network's [maximum TTL](../../../learn/fundamentals/contract-development/storage/state-archival.mdx#maximum-ttl), which is a network parameter (check the [network limits](https://lab.stellar.org/network-limits) in the Stellar Lab for the current value).

:::

## Extending a persistent entry

The example below assumes your contract keys its data with a symbol, which is the common case. If your contract uses a vec, a map, or a struct as its storage key, build the `ScVal` that matches instead of reaching for `scvSymbol`.

```typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

async function extendPersistentEntryTTL(
contractId: string,
storageKey: string | Buffer<ArrayBufferLike>,
sourceKeypair: StellarSdk.Keypair,
) {
const server = new Server("https://soroban-testnet.stellar.org");
const account = await server.getAccount(sourceKeypair.publicKey());
const fee = "100"; // BASE_FEE of 100 stroops

// Create an identifier for the persistent entry
const persistentEntry = new StellarSdk.xdr.LedgerKey.contractData({
contract: StellarSdk.Address.fromString(contractId).toScAddress(),
key: StellarSdk.xdr.ScVal.scvSymbol(storageKey),
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
});
const persistentEntry = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: StellarSdk.Address.fromString(contractId).toScAddress(),
key: StellarSdk.xdr.ScVal.scvSymbol(storageKey),
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
}),
);

// Build the transaction
let transaction = new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, // The number of ledgers past the LCL (last closed ledger) by which to extend. Roughly 5 days
}),
)
.setTimeout(30)
.build();
let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder =
new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, // The number of ledgers past the LCL (last closed ledger) by which to extend. Roughly 5 days
}),
)
.setTimeout(30);

// Attach Soroban transaction data
const sorobanData = new StellarSdk.SorobanDataBuilder()
.setReadOnly(persistentEntry)
.setReadOnly([persistentEntry])
.build();
transaction.setSorobanData(sorobanData);

// Prepare the transaction for signing.
transaction = await server.prepareTransaction(transaction);
transaction = await server.prepareTransaction(transaction.build());

transaction.sign(sourceKeypair);
return await server.sendTransaction(transaction);
}
```

### Extending the contract code
## Extending the contract instance

```javascript
const StellarSdk = require("@stellar/stellar-sdk");
const { Server } = require("@stellar/stellar-sdk/rpc");
`Contract.getFootprint()` hands you the ledger key for the deployed contract instance, so you don't have to assemble that one by hand.

async function extendContractCodeTTL(contractId, sourceKeypair) {
:::note

This extends the contract _instance_ only. The Wasm code the instance points to is a separate ledger entry with its own TTL, and an `extendFootprintTtl` operation only touches the keys you actually put in the read-only footprint. A live instance backed by archived code still leaves your contract unusable, so the next section covers the code entry too.

If you're coming from the Rust side, this is a real difference worth internalizing: inside a contract, `env.storage().instance().extend_ttl()` extends the instance and the code together. The operation used here does not.

:::

```typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

async function extendContractInstanceTTL(
contractId: string,
sourceKeypair: StellarSdk.Keypair,
) {
const server = new Server("https://soroban-testnet.stellar.org");
const account = await server.getAccount(sourceKeypair.publicKey());
const fee = "100";

// Get the footprint
// Get the ledger key for the contract instance
const contract = new StellarSdk.Contract(contractId);
const footprint = contract.getFootprint();

// Build the transaction
let transaction = new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, //The number of ledgers past the LCL by which to extend
footprint: footprint,
}),
)
.setTimeout(30)
let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder =
new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, //The number of ledgers past the LCL by which to extend
}),
)
.setTimeout(30);

// Attach Soroban transaction data
const sorobanData = new StellarSdk.SorobanDataBuilder()
.setReadOnly([footprint])
.build();
transaction.setSorobanData(sorobanData);

// Prepare the transaction
transaction = await server.prepareTransaction(transaction.build());

transaction.sign(sourceKeypair);
return await server.sendTransaction(transaction);
}
```

## Extending the contract code

The code entry is keyed by the Wasm hash rather than by the contract address, and there's no local way to derive one from the other. That means one extra round trip: read the contract's instance entry from the network, pull the Wasm hash out of its executable, and build a `contractCode` ledger key from it.

It's worth knowing every contract deployed from the same Wasm shares that single code entry, so extending it benefits all of them at once.

```typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

// Look up the hash of the Wasm code that a deployed contract runs.
async function getWasmHash(server: Server, contractId: string) {
const contract = new StellarSdk.Contract(contractId);
const { entries } = await server.getLedgerEntries(contract.getFootprint());

if (!entries || entries.length === 0) {
throw new Error(`No instance entry found for ${contractId}`);
}

const instance = entries[0].val.contractData().val().instance();
return instance.executable().wasmHash();
}

async function extendContractCodeTTL(
contractId: string,
sourceKeypair: StellarSdk.Keypair,
) {
const server = new Server("https://soroban-testnet.stellar.org");
const account = await server.getAccount(sourceKeypair.publicKey());
const fee = "100";

// Create an identifier for the Wasm code entry
const codeEntry = StellarSdk.xdr.LedgerKey.contractCode(
new StellarSdk.xdr.LedgerKeyContractCode({
hash: await getWasmHash(server, contractId),
}),
);

// Build the transaction
let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder =
new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, // The number of ledgers past the LCL by which to extend
}),
)
.setTimeout(30);

// Attach Soroban transaction data
const sorobanData = new StellarSdk.SorobanDataBuilder()
.setFootprint(footprint)
.setReadOnly([codeEntry])
.build();
transaction.setSorobanData(sorobanData);

// Prepare the transaction
transaction = await server.prepareTransaction(transaction);
transaction = await server.prepareTransaction(transaction.build());

transaction.sign(sourceKeypair);
return await server.sendTransaction(transaction);
}
```

### Extending both the contract and a persistent entry
:::caution

`getWasmHash` assumes the contract runs an executable that _was deployed_. A [Stellar Asset Contract](../../../tokens/stellar-asset-contract.mdx) instance has a built-in executable instead of a Wasm hash, so `executable().wasmHash()` will throw for one of those. Check `instance.executable().switch()` first if your code has to handle both.

:::

## Extending all three together

In Stellar, each Soroban operation must be the only Soroban operation in a single transaction. Additionally, persistent ledger entries have their own TTL values that are evaluated independently. Because the TTL extension operation only accepts one ledger footprint per transaction, you cannot combine extensions for different ledger entries in a single transaction. Instead, you must create two separate transactions to ensure each ledger key is extended correctly.
A Soroban operation has to be the only operation in its transaction, so you might expect to need one transaction per entry. Happily, you don't. A single `ExtendFootprintTTLOp` extends _every_ entry listed in the transaction's read-only footprint, applying the same `extendTo` target to all of them. When a batch of entries should live until the same ledger, list them all in one footprint and extend them together. Here's all three at once, reusing the `getWasmHash` helper from the previous section:

```typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

```javascript
async function extendContractAndPersistentEntry(
contractId,
storageKey,
sourceKeypair,
contractId: string,
storageKey: string | Buffer<ArrayBufferLike>,
sourceKeypair: StellarSdk.Keypair,
) {
// Extend the persistent entry
const persistentResult = await extendPersistentEntryTTL(
contractId,
storageKey,
sourceKeypair,
const server = new Server("https://soroban-testnet.stellar.org");
const account = await server.getAccount(sourceKeypair.publicKey());
const fee = "100";

// Ledger key for the persistent entry...
const persistentEntry = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: StellarSdk.Address.fromString(contractId).toScAddress(),
key: StellarSdk.xdr.ScVal.scvSymbol(storageKey),
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
}),
);

// ...the contract instance...
const contract = new StellarSdk.Contract(contractId);

// ...and the Wasm code that instance runs.
const codeEntry = StellarSdk.xdr.LedgerKey.contractCode(
new StellarSdk.xdr.LedgerKeyContractCode({
hash: await getWasmHash(server, contractId),
}),
);

// Extend the contract code
const contractResult = await extendContractCodeTTL(contractId, sourceKeypair);
return { persistentResult, contractResult };
// A single ExtendFootprintTtl operation extends the whole read-only footprint.
let transaction: StellarSdk.Transaction | StellarSdk.TransactionBuilder =
new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.extendFootprintTtl({
extendTo: 100_000, // one shared target, applied to every key below
}),
)
.setTimeout(30);

// List every key to extend together in one read-only footprint.
const sorobanData = new StellarSdk.SorobanDataBuilder()
.setReadOnly([persistentEntry, contract.getFootprint(), codeEntry])
.build();
transaction.setSorobanData(sorobanData);

transaction = await server.prepareTransaction(transaction.build());
transaction.sign(sourceKeypair);
return await server.sendTransaction(transaction);
}
```

Usage example:
That shared `extendTo` is the one real constraint. Entries that need _different_ target TTLs can't share an operation, so extend each one in its own transaction. That's exactly what the `extendPersistentEntryTTL`, `extendContractInstanceTTL`, and `extendContractCodeTTL` helpers above do: each builds its own operation, so each can carry its own `extendTo` target.

:::note

Batching keys into one footprint grows the transaction's resource usage, since `diskReadBytes` has to cover the serialized size of every entry in the read-only set. `server.prepareTransaction` works that out for you, but a large enough batch can bump into the network's transaction limits. If that happens, split the batch across a few transactions.

:::

Putting one of these to work looks like this:

```javascript
```typescript
const contractId = "CBEE2DHGMYRKJKSJO5E55LBKFMPXE57ZWKTXTCRMC5ANIRJ7IW2Y2WVE";
const storageKey = "BALANCE";
const sourceKeypair = StellarSdk.Keypair.fromSecret(
Expand Down
Loading