Skip to content

Commit

Permalink
Include expiration entry on ledger entries in getContractData/getLedg…
Browse files Browse the repository at this point in the history
…erEntries responses (#161)
  • Loading branch information
sreuland authored Oct 12, 2023
1 parent f3c5c41 commit 4fe4c30
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ A breaking change should be clearly marked in this log.

## Unreleased

## Added
* Include `expiration` attribute on ledger data entries in `getContractData` and `getLedgerEntries` responses ([#153](https://github.com/stellar/js-soroban-client/pull/153)).

### Breaking Changes
* All endpoints will now automatically decode XDR structures whenever possible. In particular,
- For the `Server.getLedgerEntries` response ([#154](https://github.com/stellar/js-soroban-client/pull/154)), we parse:
Expand Down
3 changes: 2 additions & 1 deletion src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ export function parseRawLedgerEntries(
latestLedger: raw.latestLedger,
entries: (raw.entries ?? []).map(rawEntry => {
if (!rawEntry.key || !rawEntry.xdr) {
throw new TypeError(`invalid ledger entry: ${rawEntry}`);
throw new TypeError(`invalid ledger entry: ${JSON.stringify(rawEntry)}`);
}

return {
lastModifiedLedgerSeq: rawEntry.lastModifiedLedgerSeq,
key: xdr.LedgerKey.fromXDR(rawEntry.key, 'base64'),
val: xdr.LedgerEntryData.fromXDR(rawEntry.xdr, 'base64'),
expirationLedgerSeq: rawEntry.expirationLedgerSeq
};
})
};
Expand Down
56 changes: 54 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Keypair,
Transaction,
xdr,
hash
} from "stellar-base";

import AxiosClient from "./axios";
Expand Down Expand Up @@ -170,6 +171,7 @@ export class Server {
* const key = xdr.ScVal.scvSymbol("counter");
* server.getContractData(contractId, key, Durability.Temporary).then(data => {
* console.log("value:", data.val);
* console.log("expiration:", data.expiration);
* console.log("lastModified:", data.lastModifiedLedgerSeq);
* console.log("latestLedger:", data.latestLedger);
* });
Expand Down Expand Up @@ -259,6 +261,7 @@ export class Server {
* const ledgerData = response.entries[0];
* console.log("key:", ledgerData.key);
* console.log("value:", ledgerData.val);
* console.log("expiration:", ledgerData.expiration);
* console.log("lastModified:", ledgerData.lastModifiedLedgerSeq);
* console.log("latestLedger:", response.latestLedger);
* });
Expand All @@ -275,8 +278,8 @@ export class Server {
return jsonrpc.post<SorobanRpc.RawGetLedgerEntriesResponse>(
this.serverURL.toString(),
"getLedgerEntries",
keys.map((k) => k.toXDR("base64")),
);
expandRequestIncludeExpirationLedgers(keys).map((k) => k.toXDR("base64")),
).then(response => mergeResponseExpirationLedgers(response, keys));
}

/**
Expand Down Expand Up @@ -749,3 +752,52 @@ function findCreatedAccountSequenceInTransactionMeta(

throw new Error("No account created in transaction");
}


// TODO - remove once rpc updated to
// append expiration entry per data LK requested onto server-side response
// https://github.com/stellar/soroban-tools/issues/1010
function mergeResponseExpirationLedgers(ledgerEntriesResponse: SorobanRpc.RawGetLedgerEntriesResponse,
requestedKeys: xdr.LedgerKey[]
): SorobanRpc.RawGetLedgerEntriesResponse {
const requestedKeyXdrs = new Set<String>(requestedKeys.map(requestedKey =>
requestedKey.toXDR('base64')));
const expirationKeyToRawEntryResult = new Map<String, SorobanRpc.RawLedgerEntryResult>();
(ledgerEntriesResponse.entries ?? []).forEach(rawEntryResult => {
if (!rawEntryResult.key || !rawEntryResult.xdr) {
throw new TypeError(`invalid ledger entry: ${JSON.stringify(rawEntryResult)}`);
}
const parsedKey = xdr.LedgerKey.fromXDR(rawEntryResult.key, 'base64');
const isExpirationMeta = parsedKey.switch().value === xdr.LedgerEntryType.expiration().value &&
!requestedKeyXdrs.has(rawEntryResult.key);
const keyHash = (isExpirationMeta)
? parsedKey.expiration().keyHash().toString()
: hash(parsedKey.toXDR()).toString();

const rawEntry = expirationKeyToRawEntryResult.get(keyHash) ?? rawEntryResult;

if (isExpirationMeta) {
const expirationLedgerSeq = xdr.LedgerEntryData
.fromXDR(rawEntryResult.xdr, 'base64')
.expiration().expirationLedgerSeq();
expirationKeyToRawEntryResult.set(keyHash, { ...rawEntry, expirationLedgerSeq});
} else {
expirationKeyToRawEntryResult.set(keyHash, { ...rawEntry, ...rawEntryResult});
}
});

ledgerEntriesResponse.entries = [...expirationKeyToRawEntryResult.values()];
return ledgerEntriesResponse;
}

// TODO - remove once rpc updated to
// include expiration entry on responses for any data LK's requested
// https://github.com/stellar/soroban-tools/issues/1010
function expandRequestIncludeExpirationLedgers(
keys: xdr.LedgerKey[]
): xdr.LedgerKey[] {
return keys.concat(keys
.filter(key => key.switch().value !== xdr.LedgerEntryType.expiration().value )
.map(key => xdr.LedgerKey.expiration(new xdr.LedgerKeyExpiration({ keyHash: hash(key.toXDR())})))
);
}
7 changes: 6 additions & 1 deletion src/soroban_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export namespace SorobanRpc {
export interface LedgerEntryResult {
lastModifiedLedgerSeq?: number;
key: xdr.LedgerKey;
val: xdr.LedgerEntryData;
val: xdr.LedgerEntryData;
expirationLedgerSeq?: number;
}

export interface RawLedgerEntryResult {
Expand All @@ -36,6 +37,10 @@ export namespace SorobanRpc {
key: string;
/** a base-64 encoded {@link xdr.LedgerEntryData} instance */
xdr: string;
/** optional, a future ledger number upon which this entry will expire
* based on https://github.com/stellar/soroban-tools/issues/1010
*/
expirationLedgerSeq?: number;
}

/** An XDR-parsed version of {@link RawLedgerEntryResult} */
Expand Down
9 changes: 6 additions & 3 deletions test/unit/server/get_account_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Account, Keypair, StrKey, xdr } = SorobanClient;
const { Account, Keypair, StrKey, xdr, hash } = SorobanClient;

describe('Server#getAccount', function () {
beforeEach(function () {
Expand All @@ -16,6 +16,9 @@ describe('Server#getAccount', function () {
const key = xdr.LedgerKey.account(new xdr.LedgerKeyAccount({ accountId }));
const accountEntry =
'AAAAAAAAAABzdv3ojkzWHMD7KUoXhrPx0GH18vHKV0ZfqpMiEblG1g3gtpoE608YAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAQAAAAAY9D8iA';
const ledgerExpirationKey = xdr.LedgerKey.expiration(
new xdr.LedgerKeyExpiration({ keyHash: hash(key.toXDR()) })
);

it('requests the correct method', function (done) {
this.axiosMock
Expand All @@ -24,7 +27,7 @@ describe('Server#getAccount', function () {
jsonrpc: '2.0',
id: 1,
method: 'getLedgerEntries',
params: [[key.toXDR('base64')]]
params: [[key.toXDR('base64'), ledgerExpirationKey.toXDR('base64')]]
})
.returns(
Promise.resolve({
Expand Down Expand Up @@ -64,7 +67,7 @@ describe('Server#getAccount', function () {
jsonrpc: '2.0',
id: 1,
method: 'getLedgerEntries',
params: [[key.toXDR('base64')]]
params: [[key.toXDR('base64'), ledgerExpirationKey.toXDR('base64')]]
})
.returns(
Promise.resolve({
Expand Down
92 changes: 81 additions & 11 deletions test/unit/server/get_contract_data_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { xdr, nativeToScVal, Durability } = SorobanClient;
const { xdr, nativeToScVal, Durability, hash } = SorobanClient;

describe('Server#getContractData', function () {
beforeEach(function () {
Expand Down Expand Up @@ -33,7 +33,69 @@ describe('Server#getContractData', function () {
})
);

it('key found', function (done) {
const ledgerExpirationKey = xdr.LedgerKey.expiration(
new xdr.LedgerKeyExpiration({ keyHash: hash(ledgerKey.toXDR()) })
);

const ledgerExpirationEntry = xdr.LedgerEntryData.expiration(
new xdr.ExpirationEntry({
keyHash: hash(ledgerKey.toXDR()),
expirationLedgerSeq: 1000
})
);

it('contract data key found', function (done) {
let result = {
lastModifiedLedgerSeq: 1,
key: ledgerKey,
val: ledgerEntry,
expirationLedgerSeq: 1000
};

this.axiosMock
.expects('post')
.withArgs(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'getLedgerEntries',
params: [
[ledgerKey.toXDR('base64'), ledgerExpirationKey.toXDR('base64')]
]
})
.returns(
Promise.resolve({
data: {
result: {
latestLedger: 420,
entries: [
{
lastModifiedLedgerSeq: result.lastModifiedLedgerSeq,
key: ledgerKey.toXDR('base64'),
xdr: ledgerEntry.toXDR('base64')
},
{
lastModifiedLedgerSeq: result.lastModifiedLedgerSeq,
key: ledgerExpirationKey.toXDR('base64'),
xdr: ledgerExpirationEntry.toXDR('base64')
}
]
}
}
})
);

this.server
.getContractData(address, key, Durability.Persistent)
.then(function (response) {
expect(response.key.toXDR('base64')).to.eql(result.key.toXDR('base64'));
expect(response.val.toXDR('base64')).to.eql(result.val.toXDR('base64'));
expect(response.expirationLedgerSeq).to.eql(1000);
done();
})
.catch((err) => done(err));
});

it('expiration entry not present for contract data key in server response', function (done) {
let result = {
lastModifiedLedgerSeq: 1,
key: ledgerKey,
Expand All @@ -46,7 +108,9 @@ describe('Server#getContractData', function () {
jsonrpc: '2.0',
id: 1,
method: 'getLedgerEntries',
params: [[ledgerKey.toXDR('base64')]]
params: [
[ledgerKey.toXDR('base64'), ledgerExpirationKey.toXDR('base64')]
]
})
.returns(
Promise.resolve({
Expand All @@ -68,31 +132,37 @@ describe('Server#getContractData', function () {
this.server
.getContractData(address, key, Durability.Persistent)
.then(function (response) {
expect(response.key.toXDR('base64')).to.be.deep.equal(
result.key.toXDR('base64')
);
expect(response.val.toXDR('base64')).to.be.deep.equal(
result.val.toXDR('base64')
);
expect(response.key.toXDR('base64')).to.eql(result.key.toXDR('base64'));
expect(response.val.toXDR('base64')).to.eql(result.val.toXDR('base64'));
expect(response.expirationLedgerSeq).to.be.undefined;
done();
})
.catch((err) => done(err));
});

it('key not found', function (done) {
it('contract data key not found', function (done) {
// clone and change durability to test this case
const ledgerKeyDupe = xdr.LedgerKey.fromXDR(ledgerKey.toXDR());
ledgerKeyDupe
.contractData()
.durability(xdr.ContractDataDurability.temporary());

const ledgerExpirationKeyDupe = xdr.LedgerKey.expiration(
new xdr.LedgerKeyExpiration({ keyHash: hash(ledgerKeyDupe.toXDR()) })
);

this.axiosMock
.expects('post')
.withArgs(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'getLedgerEntries',
params: [[ledgerKeyDupe.toXDR('base64')]]
params: [
[
ledgerKeyDupe.toXDR('base64'),
ledgerExpirationKeyDupe.toXDR('base64')
]
]
})
.returns(Promise.resolve({ data: { result: { entries: [] } } }));

Expand Down
Loading

0 comments on commit 4fe4c30

Please sign in to comment.