Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
Implement SEP-8 helper to get approval server url (#210)
Browse files Browse the repository at this point in the history
What

This PR implements the final helper function to get the approval server's url from the stellar.toml file at the issuer's home domain.

It also piggybacks a test enhancement for the getRegulatedAssetsInTx.

Why

Approval Provider relies on a valid approval server URL for initialization.
  • Loading branch information
howardtw authored Feb 23, 2021
1 parent 935d572 commit 99eadf1
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
14 changes: 8 additions & 6 deletions plans/sep8.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RegulatedAssetInfo>;
// This function returns the regulated assets found in the transaction, if any.
type getRegulatedAssetsInTx = (
params: Transaction,
) => Promise<RegulatedAssetInfo[]>;

// 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<string>;
Expand Down Expand Up @@ -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;
Expand Down
154 changes: 154 additions & 0 deletions src/sep8/getApprovalServerUrl.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
38 changes: 38 additions & 0 deletions src/sep8/getApprovalServerUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { StellarTomlResolver } from "stellar-sdk";
import { RegulatedAssetInfo } from "../types/sep8";

export async function getApprovalServerUrl(
param: RegulatedAssetInfo,
opts: StellarTomlResolver.StellarTomlResolveOptions = {},
): Promise<string> {
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}`,
);
}
31 changes: 31 additions & 0 deletions src/sep8/getRegulatedAssetsinTx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 99eadf1

Please sign in to comment.