From fd195ed66bab2c5ec2a246252b7afb83ae22c3fc Mon Sep 17 00:00:00 2001 From: George Date: Fri, 13 Sep 2024 15:55:23 -0700 Subject: [PATCH 01/11] Add helper for fetching a contract's balance --- src/rpc/api.ts | 13 ++ src/rpc/server.ts | 109 ++++++++++- .../soroban/get_contract_balance_test.js | 174 ++++++++++++++++++ 3 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 test/unit/server/soroban/get_contract_balance_test.js diff --git a/src/rpc/api.ts b/src/rpc/api.ts index d1e3addf4..eea221bc3 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -448,4 +448,17 @@ export namespace Api { transactionCount: string; // uint32 ledgerCount: number; // uint32 } + + export interface ContractBalanceResponse { + latestLedger: number; + // present only on success, otherwise request malformed or no balance + trustline?: { + balance: string; + authorized: boolean; + clawback: boolean; + + lastModifiedLedgerSeq?: number; + liveUntilLedgerSeq?: number; + }; + } } diff --git a/src/rpc/server.ts b/src/rpc/server.ts index b3e0d70e3..880430c99 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -4,10 +4,14 @@ import URI from 'urijs'; import { Account, Address, + Asset, Contract, FeeBumpTransaction, Keypair, + StrKey, Transaction, + nativeToScVal, + scValToNative, xdr } from '@stellar/stellar-base'; @@ -81,7 +85,7 @@ function findCreatedAccountSequenceInTransactionMeta( ?.account() ?.seqNum() ?.toString(); - + if (sequenceNumber) { return sequenceNumber; } @@ -878,9 +882,9 @@ export class Server { } /** - * Provides an analysis of the recent fee stats for regular and smart + * Provides an analysis of the recent fee stats for regular and smart * contract operations. - * + * * @returns {Promise} the fee stats * @see https://developers.stellar.org/docs/data/rpc/api-reference/methods/getFeeStats */ @@ -898,4 +902,103 @@ export class Server { return jsonrpc.postObject(this.serverURL.toString(), 'getVersionInfo'); } + /** + * Returns a contract's balance of a particular token, if any. + * + * This is a convenience wrapper around {@link Server.getLedgerEntries}. + * + * @param {string} contractId the contract ID (string `C...`) whose + * balance of `token` you want to know + * @param {Asset} token the token or asset (e.g. `USDC:GABC...`) that + * you are querying from the given `contract`. + * @param {string} [networkPassphrase] optionally, the network passphrase to + * which this token applies. If omitted, a request about network + * information will be made (see {@link getNetwork}), since contract IDs + * for assets are specific to a network. You can refer to {@link Networks} + * for a list of built-in passphrases, e.g., `Networks.TESTNET`. + * + * @returns {Promise}, which will contain the + * trustline details if and only if the request returned a valid balance + * ledger entry. If it doesn't, the `trustline` field will not exist. + * + * @throws {TypeError} If `contractId` is not a valid contract strkey (C...). + * + * @warning This should not be used for fetching custom token contracts, only + * SACs. Using them with custom tokens is a security concern because they + * can format their balance entries in any way they want, and thus this + * fetch can be very misleading. + * + * @see getLedgerEntries + */ + public async getContractBalance( + contractId: string, + token: Asset, + networkPassphrase?: string + ): Promise { + if (!StrKey.isValidContract(contractId)) { + throw new TypeError(`expected contract ID, got ${contractId}`); + } + + // Call out to RPC if passphrase isn't provided. + const passphrase: string = networkPassphrase + ?? await this.getNetwork().then(n => n.passphrase); + + // Turn token into predictable contract ID + const tokenId = token.contractId(passphrase); + + // Rust union enum type with "Balance(ScAddress)" structure + const key = xdr.ScVal.scvVec([ + nativeToScVal("Balance", { type: "symbol" }), + nativeToScVal(contractId, { type: "address" }), + ]); + + // Note a quirk here: the contract address in the key is the *token* + // rather than the *holding contract*. This is because each token stores a + // balance entry for each contract, not the other way around (i.e. XLM + // holds a reserve for contract X, rather that contract X having a balance + // of N XLM). + const ledgerKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: new Address(tokenId).toScAddress(), + durability: xdr.ContractDataDurability.persistent(), + key + }) + ); + + const response = await this.getLedgerEntries(ledgerKey); + if (response.entries.length === 0) { + return { latestLedger: response.latestLedger }; + } + + const { + lastModifiedLedgerSeq, + liveUntilLedgerSeq, + val + } = response.entries[0]; + + if (val.switch().value !== xdr.LedgerEntryType.contractData().value) { + return { latestLedger: response.latestLedger }; + } + + // If any field doesn't match *exactly* what we expect, we bail. This + // prevents confusion with "balance-like" entries (e.g., has `amount` but + // isn't a bigint), but still allows "looks like a duck" balance entries. + const entry = scValToNative(val.contractData().val()); + if (typeof entry.amount === 'bigint' && + typeof entry.authorized === 'boolean' && + typeof entry.clawback === 'boolean') { + return { + latestLedger: response.latestLedger, + trustline: { + liveUntilLedgerSeq, + lastModifiedLedgerSeq, + balance: entry.amount.toString(), + authorized: entry.authorized, + clawback: entry.clawback, + } + }; + } + + return { latestLedger: response.latestLedger }; + } } diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js new file mode 100644 index 000000000..a23021eae --- /dev/null +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -0,0 +1,174 @@ +const { Address, xdr, nativeToScVal, hash } = StellarSdk; +const { Server, AxiosClient, Durability } = StellarSdk.rpc; + +describe("Server#getContractBalance", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + const token = StellarSdk.Asset.native(); + const contract = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5"; + const contractAddress = new Address( + token.contractId(StellarSdk.Networks.TESTNET), + ).toScAddress(); + + const key = xdr.ScVal.scvVec([ + nativeToScVal("Balance", { type: "symbol" }), + nativeToScVal(contract, { type: "address" }), + ]); + const val = nativeToScVal( + { + amount: 1_000_000_000_000n, + clawback: false, + authorized: true, + }, + { + type: { + amount: ["symbol", "i128"], + clawback: ["symbol", "boolean"], + authorized: ["symbol", "boolean"], + }, + }, + ); + + const contractBalanceEntry = xdr.LedgerEntryData.contractData( + new xdr.ContractDataEntry({ + ext: new xdr.ExtensionPoint(0), + contract: contractAddress, + durability: xdr.ContractDataDurability.persistent(), + key, + val, + }), + ); + + // key is just a subset of the entry + const contractBalanceKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractBalanceEntry.contractData().contract(), + durability: contractBalanceEntry.contractData().durability(), + key: contractBalanceEntry.contractData().key(), + }), + ); + + function buildMockResult(that, entry) { + let result = { + latestLedger: 1000, + entries: [ + { + lastModifiedLedgerSeq: 1, + liveUntilLedgerSeq: 1000, + key: contractBalanceKey.toXDR("base64"), + xdr: entry.toXDR("base64"), + }, + ], + }; + + that.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: { keys: [contractBalanceKey.toXDR("base64")] }, + }) + .returns( + Promise.resolve({ + data: { result }, + }), + ); + } + + it("returns the correct trustline", function (done) { + buildMockResult(this, contractBalanceEntry); + + this.server + .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) + .then((response) => { + expect(response.latestLedger).to.equal(1000); + expect(response.trustline).to.not.be.undefined; + expect(response.trustline.balance).to.equal("1000000000000"); + expect(response.trustline.authorized).to.be.true; + expect(response.trustline.clawback).to.be.false; + done(); + }) + .catch((err) => done(err)); + }); + + it("infers the network passphrase", function (done) { + buildMockResult(this, contractBalanceEntry); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getNetwork", + params: null, + }) + .returns( + Promise.resolve({ + data: { + result: { + passphrase: StellarSdk.Networks.TESTNET, + }, + }, + }), + ); + + this.server + .getContractBalance(contract, token) + .then((response) => { + expect(response.latestLedger).to.equal(1000); + expect(response.trustline).to.not.be.undefined; + expect(response.trustline.balance).to.equal("1000000000000"); + expect(response.trustline.authorized).to.be.true; + expect(response.trustline.clawback).to.be.false; + done(); + }) + .catch((err) => done(err)); + }); + + it("errors out when the entry isn't valid", function (done) { + // this doesn't conform to the expected format + const invalidVal = nativeToScVal( + { + amount: 1_000_000, // not an i128 + clawback: "false", // not a bool + authorized: true, + }, + { + type: { + amount: ["symbol", "u64"], + clawback: ["symbol", "string"], + authorized: ["symbol", "boolean"], + }, + }, + ); + const invalidEntry = xdr.LedgerEntryData.contractData( + new xdr.ContractDataEntry({ + ext: new xdr.ExtensionPoint(0), + contract: contractAddress, + durability: xdr.ContractDataDurability.persistent(), + val: invalidVal, + key, + }), + ); + + buildMockResult(this, invalidEntry); + + this.server + .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) + .then((response) => { + expect(response.latestLedger).to.equal(1000); + expect(response.trustline).to.be.undefined; + done(); + }) + .catch((err) => done(err)); + }); +}); From f527390dcd66b9281437206547dcd38fea711c76 Mon Sep 17 00:00:00 2001 From: George Date: Fri, 13 Sep 2024 17:22:08 -0700 Subject: [PATCH 02/11] Remove invalid entry case because it's impossible --- src/rpc/server.ts | 38 ++++++--------- .../soroban/get_contract_balance_test.js | 46 ++----------------- 2 files changed, 18 insertions(+), 66 deletions(-) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 880430c99..437e65572 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -909,7 +909,7 @@ export class Server { * * @param {string} contractId the contract ID (string `C...`) whose * balance of `token` you want to know - * @param {Asset} token the token or asset (e.g. `USDC:GABC...`) that + * @param {Asset} token the token/asset (e.g. `USDC:GABC...`) that * you are querying from the given `contract`. * @param {string} [networkPassphrase] optionally, the network passphrase to * which this token applies. If omitted, a request about network @@ -923,11 +923,6 @@ export class Server { * * @throws {TypeError} If `contractId` is not a valid contract strkey (C...). * - * @warning This should not be used for fetching custom token contracts, only - * SACs. Using them with custom tokens is a security concern because they - * can format their balance entries in any way they want, and thus this - * fetch can be very misleading. - * * @see getLedgerEntries */ public async getContractBalance( @@ -980,25 +975,20 @@ export class Server { return { latestLedger: response.latestLedger }; } - // If any field doesn't match *exactly* what we expect, we bail. This - // prevents confusion with "balance-like" entries (e.g., has `amount` but - // isn't a bigint), but still allows "looks like a duck" balance entries. const entry = scValToNative(val.contractData().val()); - if (typeof entry.amount === 'bigint' && - typeof entry.authorized === 'boolean' && - typeof entry.clawback === 'boolean') { - return { - latestLedger: response.latestLedger, - trustline: { - liveUntilLedgerSeq, - lastModifiedLedgerSeq, - balance: entry.amount.toString(), - authorized: entry.authorized, - clawback: entry.clawback, - } - }; - } - return { latestLedger: response.latestLedger }; + // Since we are requesting a SAC's contract data, we know for a fact that + // it should follow the expected structure format. Thus, we can presume + // these fields exist: + return { + latestLedger: response.latestLedger, + trustline: { + liveUntilLedgerSeq, + lastModifiedLedgerSeq, + balance: entry.amount.toString(), + authorized: entry.authorized, + clawback: entry.clawback, + } + }; } } diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index a23021eae..25e123065 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -56,7 +56,7 @@ describe("Server#getContractBalance", function () { }), ); - function buildMockResult(that, entry) { + function buildMockResult(that) { let result = { latestLedger: 1000, entries: [ @@ -64,7 +64,7 @@ describe("Server#getContractBalance", function () { lastModifiedLedgerSeq: 1, liveUntilLedgerSeq: 1000, key: contractBalanceKey.toXDR("base64"), - xdr: entry.toXDR("base64"), + xdr: contractBalanceEntry.toXDR("base64"), }, ], }; @@ -85,7 +85,7 @@ describe("Server#getContractBalance", function () { } it("returns the correct trustline", function (done) { - buildMockResult(this, contractBalanceEntry); + buildMockResult(this); this.server .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) @@ -101,7 +101,7 @@ describe("Server#getContractBalance", function () { }); it("infers the network passphrase", function (done) { - buildMockResult(this, contractBalanceEntry); + buildMockResult(this); this.axiosMock .expects("post") @@ -133,42 +133,4 @@ describe("Server#getContractBalance", function () { }) .catch((err) => done(err)); }); - - it("errors out when the entry isn't valid", function (done) { - // this doesn't conform to the expected format - const invalidVal = nativeToScVal( - { - amount: 1_000_000, // not an i128 - clawback: "false", // not a bool - authorized: true, - }, - { - type: { - amount: ["symbol", "u64"], - clawback: ["symbol", "string"], - authorized: ["symbol", "boolean"], - }, - }, - ); - const invalidEntry = xdr.LedgerEntryData.contractData( - new xdr.ContractDataEntry({ - ext: new xdr.ExtensionPoint(0), - contract: contractAddress, - durability: xdr.ContractDataDurability.persistent(), - val: invalidVal, - key, - }), - ); - - buildMockResult(this, invalidEntry); - - this.server - .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) - .then((response) => { - expect(response.latestLedger).to.equal(1000); - expect(response.trustline).to.be.undefined; - done(); - }) - .catch((err) => done(err)); - }); }); From bb7771ba3f088283f74f5c2a3d3b60acf3ffade4 Mon Sep 17 00:00:00 2001 From: George Date: Fri, 13 Sep 2024 17:37:42 -0700 Subject: [PATCH 03/11] Rename variables to match the truth better --- src/rpc/api.ts | 4 ++-- src/rpc/server.ts | 4 ++-- .../server/soroban/get_contract_balance_test.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/rpc/api.ts b/src/rpc/api.ts index eea221bc3..0de188dc6 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -452,8 +452,8 @@ export namespace Api { export interface ContractBalanceResponse { latestLedger: number; // present only on success, otherwise request malformed or no balance - trustline?: { - balance: string; + balanceEntry?: { + amount: string; authorized: boolean; clawback: boolean; diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 437e65572..ec051e0c3 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -982,10 +982,10 @@ export class Server { // these fields exist: return { latestLedger: response.latestLedger, - trustline: { + balanceEntry: { liveUntilLedgerSeq, lastModifiedLedgerSeq, - balance: entry.amount.toString(), + amount: entry.amount.toString(), authorized: entry.authorized, clawback: entry.clawback, } diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index 25e123065..1228b0782 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -91,10 +91,10 @@ describe("Server#getContractBalance", function () { .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) .then((response) => { expect(response.latestLedger).to.equal(1000); - expect(response.trustline).to.not.be.undefined; - expect(response.trustline.balance).to.equal("1000000000000"); - expect(response.trustline.authorized).to.be.true; - expect(response.trustline.clawback).to.be.false; + expect(response.balanceEntry).to.not.be.undefined; + expect(response.balanceEntry.amount).to.equal("1000000000000"); + expect(response.balanceEntry.authorized).to.be.true; + expect(response.balanceEntry.clawback).to.be.false; done(); }) .catch((err) => done(err)); @@ -125,10 +125,10 @@ describe("Server#getContractBalance", function () { .getContractBalance(contract, token) .then((response) => { expect(response.latestLedger).to.equal(1000); - expect(response.trustline).to.not.be.undefined; - expect(response.trustline.balance).to.equal("1000000000000"); - expect(response.trustline.authorized).to.be.true; - expect(response.trustline.clawback).to.be.false; + expect(response.balanceEntry).to.not.be.undefined; + expect(response.balanceEntry.amount).to.equal("1000000000000"); + expect(response.balanceEntry.authorized).to.be.true; + expect(response.balanceEntry.clawback).to.be.false; done(); }) .catch((err) => done(err)); From 8bc0e2def8d5684adfce00d7a4a291ff1409c326 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 13:27:22 -0700 Subject: [PATCH 04/11] Rename to be specific to built-in tokens --- src/rpc/server.ts | 17 +++++++++-------- .../server/soroban/get_contract_balance_test.js | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index ec051e0c3..3d9b81951 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -903,13 +903,13 @@ export class Server { } /** - * Returns a contract's balance of a particular token, if any. + * Returns a contract's balance of a particular SAC asset, if any. * * This is a convenience wrapper around {@link Server.getLedgerEntries}. * * @param {string} contractId the contract ID (string `C...`) whose - * balance of `token` you want to know - * @param {Asset} token the token/asset (e.g. `USDC:GABC...`) that + * balance of `sac` you want to know + * @param {Asset} sac the built-in SAC token (e.g. `USDC:GABC...`) that * you are querying from the given `contract`. * @param {string} [networkPassphrase] optionally, the network passphrase to * which this token applies. If omitted, a request about network @@ -924,10 +924,11 @@ export class Server { * @throws {TypeError} If `contractId` is not a valid contract strkey (C...). * * @see getLedgerEntries + * @see https://developers.stellar.org/docs/tokens/stellar-asset-contract */ - public async getContractBalance( + public async getSACBalance( contractId: string, - token: Asset, + sac: Asset, networkPassphrase?: string ): Promise { if (!StrKey.isValidContract(contractId)) { @@ -938,8 +939,8 @@ export class Server { const passphrase: string = networkPassphrase ?? await this.getNetwork().then(n => n.passphrase); - // Turn token into predictable contract ID - const tokenId = token.contractId(passphrase); + // Turn SAC into predictable contract ID + const sacId = sac.contractId(passphrase); // Rust union enum type with "Balance(ScAddress)" structure const key = xdr.ScVal.scvVec([ @@ -954,7 +955,7 @@ export class Server { // of N XLM). const ledgerKey = xdr.LedgerKey.contractData( new xdr.LedgerKeyContractData({ - contract: new Address(tokenId).toScAddress(), + contract: new Address(sacId).toScAddress(), durability: xdr.ContractDataDurability.persistent(), key }) diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index 1228b0782..e3ba89ea2 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -88,7 +88,7 @@ describe("Server#getContractBalance", function () { buildMockResult(this); this.server - .getContractBalance(contract, token, StellarSdk.Networks.TESTNET) + .getSACBalance(contract, token, StellarSdk.Networks.TESTNET) .then((response) => { expect(response.latestLedger).to.equal(1000); expect(response.balanceEntry).to.not.be.undefined; @@ -122,7 +122,7 @@ describe("Server#getContractBalance", function () { ); this.server - .getContractBalance(contract, token) + .getSACBalance(contract, token) .then((response) => { expect(response.latestLedger).to.equal(1000); expect(response.balanceEntry).to.not.be.undefined; From 8d444b40fbed8a2ad94b39803156f30b1349e71a Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 13:28:51 -0700 Subject: [PATCH 05/11] Finish the rename --- src/rpc/api.ts | 2 +- src/rpc/server.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rpc/api.ts b/src/rpc/api.ts index 0de188dc6..c4d6408e2 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -449,7 +449,7 @@ export namespace Api { ledgerCount: number; // uint32 } - export interface ContractBalanceResponse { + export interface BalanceResponse { latestLedger: number; // present only on success, otherwise request malformed or no balance balanceEntry?: { diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 3d9b81951..733eab0e4 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -917,7 +917,7 @@ export class Server { * for assets are specific to a network. You can refer to {@link Networks} * for a list of built-in passphrases, e.g., `Networks.TESTNET`. * - * @returns {Promise}, which will contain the + * @returns {Promise}, which will contain the * trustline details if and only if the request returned a valid balance * ledger entry. If it doesn't, the `trustline` field will not exist. * @@ -930,7 +930,7 @@ export class Server { contractId: string, sac: Asset, networkPassphrase?: string - ): Promise { + ): Promise { if (!StrKey.isValidContract(contractId)) { throw new TypeError(`expected contract ID, got ${contractId}`); } From 00f77d9884e1aad8057976b5875c02f09fa79eb1 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:03:47 -0700 Subject: [PATCH 06/11] Add comments to clarify types --- src/rpc/api.ts | 5 +++-- src/rpc/server.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rpc/api.ts b/src/rpc/api.ts index c4d6408e2..8d65781c4 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -411,7 +411,7 @@ export namespace Api { transactionData: string; }; - /** State Difference information */ + /** State difference information */ stateChanges?: RawLedgerEntryChange[]; } @@ -451,8 +451,9 @@ export namespace Api { export interface BalanceResponse { latestLedger: number; - // present only on success, otherwise request malformed or no balance + /** present only on success, otherwise request malformed or no balance */ balanceEntry?: { + /** a 64-bit integer */ amount: string; authorized: boolean; clawback: boolean; diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 733eab0e4..7931ba5f7 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -986,7 +986,7 @@ export class Server { balanceEntry: { liveUntilLedgerSeq, lastModifiedLedgerSeq, - amount: entry.amount.toString(), + amount: entry.amount, authorized: entry.authorized, clawback: entry.clawback, } From 72af718c7c5c6a68ee8c9702b86d4cfc3e9964df Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:30:31 -0700 Subject: [PATCH 07/11] Add example to docs --- src/rpc/server.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 7931ba5f7..d219a0ee9 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -925,6 +925,21 @@ export class Server { * * @see getLedgerEntries * @see https://developers.stellar.org/docs/tokens/stellar-asset-contract + * + * @example + * // assume `contractId` is some contract with an XLM balance + * // assume server is an instantiated `Server` instance. + * const entry = (await server.getSACBalance( + * new Address(contractId), + * Asset.native(), + * Networks.PUBLIC + * )); + * + * // assumes BigInt support: + * console.log( + * entry.balanceEntry ? + * BigInt(entry.balanceEntry.amount) : + * "Contract has no XLM"); */ public async getSACBalance( contractId: string, From 0c09180b1876e9eeff106f66a97ac8af4fd9a8d3 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:35:39 -0700 Subject: [PATCH 08/11] Update jsdoc terms to not say trustline --- src/rpc/server.ts | 8 ++++---- test/unit/server/soroban/get_contract_balance_test.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index d219a0ee9..458c0782b 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -917,9 +917,9 @@ export class Server { * for assets are specific to a network. You can refer to {@link Networks} * for a list of built-in passphrases, e.g., `Networks.TESTNET`. * - * @returns {Promise}, which will contain the - * trustline details if and only if the request returned a valid balance - * ledger entry. If it doesn't, the `trustline` field will not exist. + * @returns {Promise}, which will contain the balance + * entry details if and only if the request returned a valid balance ledger + * entry. If it doesn't, the `balanceEntry` field will not exist. * * @throws {TypeError} If `contractId` is not a valid contract strkey (C...). * @@ -1001,7 +1001,7 @@ export class Server { balanceEntry: { liveUntilLedgerSeq, lastModifiedLedgerSeq, - amount: entry.amount, + amount: entry.amount.toString(), authorized: entry.authorized, clawback: entry.clawback, } diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index e3ba89ea2..df72e6293 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -84,7 +84,7 @@ describe("Server#getContractBalance", function () { ); } - it("returns the correct trustline", function (done) { + it("returns the correct balance entry", function (done) { buildMockResult(this); this.server From 503fd0d4ca57ecd5182491b956ad916903fd28f9 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:51:31 -0700 Subject: [PATCH 09/11] Add unhappy path tests --- .../unit/server/soroban/get_contract_balance_test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index df72e6293..1b32d75bb 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -1,4 +1,4 @@ -const { Address, xdr, nativeToScVal, hash } = StellarSdk; +const { Address, Keypair, xdr, nativeToScVal, hash } = StellarSdk; const { Server, AxiosClient, Durability } = StellarSdk.rpc; describe("Server#getContractBalance", function () { @@ -133,4 +133,14 @@ describe("Server#getContractBalance", function () { }) .catch((err) => done(err)); }); + + it("throws on invalid addresses", function (done) { + expect( + this.server.getSACBalance(Keypair.random().publicKey(), token), + ).to.throw(/TypeError/); + + expect( + this.server.getSACBalance(contract.substring(0, -1), token), + ).to.throw(/TypeError/); + }); }); From 89e904b865f29114b4920b9ef74ec65e435c2691 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:55:14 -0700 Subject: [PATCH 10/11] Fix bad testing semantics for async errors --- .../soroban/get_contract_balance_test.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/unit/server/soroban/get_contract_balance_test.js b/test/unit/server/soroban/get_contract_balance_test.js index 1b32d75bb..6da8315a0 100644 --- a/test/unit/server/soroban/get_contract_balance_test.js +++ b/test/unit/server/soroban/get_contract_balance_test.js @@ -135,12 +135,19 @@ describe("Server#getContractBalance", function () { }); it("throws on invalid addresses", function (done) { - expect( - this.server.getSACBalance(Keypair.random().publicKey(), token), - ).to.throw(/TypeError/); + this.server + .getSACBalance(Keypair.random().publicKey(), token) + .then(() => done(new Error("Error didn't occur"))) + .catch((err) => { + expect(err).to.match(/TypeError/); + }); - expect( - this.server.getSACBalance(contract.substring(0, -1), token), - ).to.throw(/TypeError/); + this.server + .getSACBalance(contract.substring(0, -1), token) + .then(() => done(new Error("Error didn't occur"))) + .catch((err) => { + expect(err).to.match(/TypeError/); + done(); + }); }); }); From 92dbfb7a15d6a7960be2e604b9f587ee1e026144 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 14:57:20 -0700 Subject: [PATCH 11/11] Add changelog entry --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea800ac1..514599abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ A breaking change will get clearly marked in this log. ## Unreleased +### Added +- `rpc.Server` now has a `getSACBalance` helper which lets you fetch the balance of a built-in Stellar Asset Contract token held by a contract ([#1046](https://github.com/stellar/js-stellar-sdk/pull/1046)): + +```typescript +export interface BalanceResponse { + latestLedger: number; + /** present only on success, otherwise request malformed or no balance */ + balanceEntry?: { + /** a 64-bit integer */ + amount: string; + authorized: boolean; + clawback: boolean; + + lastModifiedLedgerSeq?: number; + liveUntilLedgerSeq?: number; + }; +} +``` + ## [v12.3.0](https://github.com/stellar/js-stellar-sdk/compare/v12.2.0...v12.3.0)