diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b145cf2..ad1c594d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## In master - Added SEP-8 Approval Provider. +- Added SEP-8 helper functions: `getRegulatedAssetsInTx()` and + `getApprovalServerUrl()`. ## [v0.3.0-rc.4](https://github.com/stellar/js-stellar-wallets/compare/v0.3.0-rc.3...v0.3.0-rc.4) diff --git a/plans/sep8.md b/plans/sep8.md index 86ff3196..6ca1381b 100644 --- a/plans/sep8.md +++ b/plans/sep8.md @@ -34,8 +34,10 @@ The high-level flow is: ### Types ```ts -// This function returns the first regulated asset found in the transaction, if any. -type getRegulatedAssetInTx = (params: Transaction) => Promise; +// This function returns the regulated assets found in the transaction, if any. +type getRegulatedAssetsInTx = ( + params: Transaction, +) => Promise; // Get the approval server's URL by fetching the stellar.toml file at the home domain and look for the matched currency. type getApprovalServerUrl = (params: RegulatedAssetInfo) => Promise; @@ -101,20 +103,20 @@ interface PostActionUrlResponse { import { ApprovalProvider, ApprovalResponseType, - getRegulatedAssetInTx, + getRegulatedAssetsInTx, getApprovalServerUrl, } from "wallet-sdk"; // Parse transaction to check if it involves regulated assets -const regulatedAsset = getRegulatedAssetInTx(transaction); -if (!regulatedAsset) { +const regulatedAssets = getRegulatedAssetsInTx(transaction); +if (!regulatedAssets.length) { // No approval needed so submit to the network submitPayment(transaction); return; } // TODO: check whether the user is already authorized to transact the asset. - +const regulatedAsset = regulatedAssets[0]; if (!regulatedAsset.home_domain) { // Report an error saying a certain information is missing in order to transact the asset. return; diff --git a/src/sep8/getApprovalServerUrl.test.ts b/src/sep8/getApprovalServerUrl.test.ts new file mode 100644 index 00000000..78df6e15 --- /dev/null +++ b/src/sep8/getApprovalServerUrl.test.ts @@ -0,0 +1,154 @@ +import axios from "axios"; +import sinon from "sinon"; +import { Config } from "stellar-sdk"; +import { getApprovalServerUrl } from "./getApprovalServerUrl"; + +describe("getApprovalServerUrl", () => { + let axiosMock: sinon.SinonMock; + + beforeEach(() => { + axiosMock = sinon.mock(axios); + Config.setDefault(); + }); + + afterEach(() => { + axiosMock.verify(); + axiosMock.restore(); + }); + + test("Issuer's Home Domain missing", async () => { + try { + // @ts-ignore + const res = await getApprovalServerUrl({ + asset_code: "USD", + asset_issuer: + "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", + }); + expect("This test failed").toBe(null); + } catch (e) { + expect(e.toString()).toMatch(`Error: Issuer's home domain is missing`); + } + }); + + test("stellar.toml CURRENCIES missing", async () => { + const homeDomain = "example.com"; + axiosMock + .expects("get") + .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) + .returns( + Promise.resolve({ + data: "", + }), + ); + + try { + // @ts-ignore + const res = await getApprovalServerUrl({ + asset_code: "USD", + asset_issuer: + "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", + home_domain: homeDomain, + }); + expect("This test failed").toBe(null); + } catch (e) { + expect(e.toString()).toMatch( + `Error: stellar.toml at ${homeDomain} does not contain CURRENCIES` + + ` field`, + ); + } + }); + + test("stellar.toml approval_server missing", async () => { + const homeDomain = "example.com"; + axiosMock + .expects("get") + .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) + .returns( + Promise.resolve({ + data: ` +[[CURRENCIES]] +code = "USD" +issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" +`, + }), + ); + + try { + // @ts-ignore + const res = await getApprovalServerUrl({ + asset_code: "USD", + asset_issuer: + "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", + home_domain: homeDomain, + }); + expect("This test failed").toBe(null); + } catch (e) { + expect(e.toString()).toMatch( + `Error: stellar.toml at ${homeDomain} does not contain` + + ` approval_server information for this asset`, + ); + } + }); + + test("stellar.toml asset not found", async () => { + const homeDomain = "example.com"; + axiosMock + .expects("get") + .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) + .returns( + Promise.resolve({ + data: ` +[[CURRENCIES]] +code = "USD" +issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" +`, + }), + ); + + try { + // @ts-ignore + const res = await getApprovalServerUrl({ + asset_code: "EUR", + asset_issuer: + "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", + home_domain: homeDomain, + }); + expect("This test failed").toBe(null); + } catch (e) { + expect(e.toString()).toMatch( + `Error: CURRENCY EUR:` + + `GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW` + + ` not found on stellar.toml at ${homeDomain}`, + ); + } + }); + + test("approval server URL is returned", async () => { + const homeDomain = "example.com"; + axiosMock + .expects("get") + .withArgs(sinon.match(`https://${homeDomain}/.well-known/stellar.toml`)) + .returns( + Promise.resolve({ + data: ` +[[CURRENCIES]] +code = "USD" +issuer = "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW" +approval_server = "https://example.com/approve" +`, + }), + ); + + try { + const res = await getApprovalServerUrl({ + asset_code: "USD", + asset_issuer: + "GDBMMVJKWGT2N6HZ2BGMFHKODASVFYIHL2VS3RUTB3B3QES2R6YFXGQW", + home_domain: homeDomain, + }); + expect(res).toEqual("https://example.com/approve"); + } catch (e) { + expect(e).toBe(null); + } + }); +}); diff --git a/src/sep8/getApprovalServerUrl.ts b/src/sep8/getApprovalServerUrl.ts new file mode 100644 index 00000000..9d2bffe1 --- /dev/null +++ b/src/sep8/getApprovalServerUrl.ts @@ -0,0 +1,38 @@ +import { StellarTomlResolver } from "stellar-sdk"; +import { RegulatedAssetInfo } from "../types/sep8"; + +export async function getApprovalServerUrl( + param: RegulatedAssetInfo, + opts: StellarTomlResolver.StellarTomlResolveOptions = {}, +): Promise { + if (!param.home_domain) { + throw new Error(`Issuer's home domain is missing`); + } + + const tomlObject = await StellarTomlResolver.resolve(param.home_domain, opts); + if (!tomlObject.CURRENCIES) { + throw new Error( + `stellar.toml at ${param.home_domain} does not contain CURRENCIES field`, + ); + } + + for (const ast of tomlObject.CURRENCIES) { + if (ast.code === param.asset_code && ast.issuer === param.asset_issuer) { + if (!ast.approval_server) { + throw new Error( + `stellar.toml at ${ + param.home_domain + } does not contain approval_server information for this asset`, + ); + } + + return ast.approval_server; + } + } + + throw new Error( + `CURRENCY ${param.asset_code}:${ + param.asset_issuer + } not found on stellar.toml at ${param.home_domain}`, + ); +} diff --git a/src/sep8/getRegulatedAssetsinTx.test.ts b/src/sep8/getRegulatedAssetsinTx.test.ts index af51ad17..b8cd32a3 100644 --- a/src/sep8/getRegulatedAssetsinTx.test.ts +++ b/src/sep8/getRegulatedAssetsinTx.test.ts @@ -431,6 +431,37 @@ describe("getRegulatedAssetsInTx with no ops moving assets", () => { } }); + test("Revoke Claimable Balance Sponsorship Op", async () => { + const account = new Account( + "GD6WU64OEP5C4LRBH6NK3MHYIA2ADN6K6II6EXPNVUR3ERBXT4AN4ACD", + "2319149195853854", + ); + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.revokeClaimableBalanceSponsorship({ + balanceId: + "00000000929b20b72e5890ab51c24f1cc46fa01c4f318d8d33367d24dd614cfd" + + "f5491072", + }), + ) + .setTimeout(30) + .build(); + + try { + const res = await getRegulatedAssetsInTx( + tx, + "https://horizon-live.stellar.org:1337", + ); + expect(res).toEqual([]); + } catch (e) { + expect(e).toBe(null); + } + }); + test("Revoke Signer Sponsorship Op returns an empty array", async () => { const account = new Account( "GD6WU64OEP5C4LRBH6NK3MHYIA2ADN6K6II6EXPNVUR3ERBXT4AN4ACD",