Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add sep24 callback params #97

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 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
22 changes: 22 additions & 0 deletions .github/workflows/npmPublish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: npm publish
on:
release:
types: [published]
jobs:
npm-publish:
name: npm-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: yarn install
- run: yarn build
- run: yarn test:ci

- name: Publish to NPM
run: yarn publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export { Utils };
import * as Exceptions from "./walletSdk/Exceptions";
export { Exceptions };

/**
* Server
*/
import * as Server from "./walletSdk/Server";
export { Server };

import * as walletSdk from "./walletSdk";
import { Keypair } from "@stellar/stellar-sdk";
// TODO - figure out why Keypair used in parent codebase throws error
Expand Down
22 changes: 22 additions & 0 deletions src/walletSdk/Anchor/Sep24.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class Sep24 {
* @param {ExtraFields} [params.extraFields] - Additional fields for the request.
* @param {Memo} [params.destinationMemo] - Memo information for the destination account.
* @param {string} [params.destinationAccount] - The destination account for the deposit.
* @param {string} [params.callback] - The callback URL or postMessage the anchor should POST to
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ifropc is postMessage something that's used / we should support?

and if so what's an example of one? just a JSON string?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not support postMessage. This is an error-prone way of signaling to the wallet to close the webview, and there are more straightforward approaches to this that have better user experiences as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We are deprecating postMessage
  2. We are also deprecating approach of passing callbacks "as is" to the request parameter. I'm working on drafting new approach, where callbacks are passed outside of the request.
    So let's hold on this PR for now

* on a successfully completed interactive flow.
* @param {string} [params.on_change_callback] - The URL or postMessage the anchor should POST to
* when the 'status' or 'kyc_verified' properties change.
* @returns {Promise<Sep24PostResponse>} The Sep24 response.
* @throws {AssetNotSupportedError} If the asset is not supported for deposit.
*/
Expand All @@ -71,6 +75,8 @@ export class Sep24 {
extraFields,
destinationMemo,
destinationAccount,
callback,
on_change_callback,
}: Sep24PostParams): Promise<Sep24PostResponse> {
return this.flow({
assetCode,
Expand All @@ -79,6 +85,8 @@ export class Sep24 {
extraFields,
destinationMemo,
account: destinationAccount,
callback,
on_change_callback,
type: FLOW_TYPE.DEPOSIT,
});
}
Expand All @@ -91,6 +99,10 @@ export class Sep24 {
* @param {string} [params.lang] - The language for the request (defaults to the Anchor's language).
* @param {ExtraFields} [params.extraFields] - Additional fields for the request.
* @param {string} [params.withdrawalAccount] - The withdrawal account.
* @param {string} [params.callback] - The callback URL or postMessage the anchor should POST to
* on a successfully completed interactive flow.
* @param {string} [params.on_change_callback] - The URL or postMessage the anchor should POST to
* when the 'status' or 'kyc_verified' properties change.
* @returns {Promise<Sep24PostResponse>} The Sep24 response.
* @throws {AssetNotSupportedError} If the asset is not supported for withdrawal.
*/
Expand All @@ -100,13 +112,17 @@ export class Sep24 {
lang,
extraFields,
withdrawalAccount,
callback,
on_change_callback,
}: Sep24PostParams): Promise<Sep24PostResponse> {
return this.flow({
assetCode,
authToken,
lang,
extraFields,
account: withdrawalAccount,
callback,
on_change_callback,
type: FLOW_TYPE.WITHDRAW,
});
}
Expand Down Expand Up @@ -164,6 +180,12 @@ export class Sep24 {

const interactiveResponse: Sep24PostResponse = resp.data;

if (params.callback) {
interactiveResponse.url = `${interactiveResponse.url}&callback=${params.callback}`;
} else if (params.on_change_callback) {
interactiveResponse.url = `${interactiveResponse.url}&on_change_callback=${params.on_change_callback}`;
}

return interactiveResponse;
} catch (e) {
throw new ServerRequestFailedError(e);
Expand Down
14 changes: 14 additions & 0 deletions src/walletSdk/Exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,17 @@ export class Sep38PriceOnlyOneAmountError extends Error {
Object.setPrototypeOf(this, Sep38PriceOnlyOneAmountError.prototype);
}
}

export class ChallengeTxnIncorrectSequenceError extends Error {
constructor() {
super("Challenge transaction sequence number must be 0");
Object.setPrototypeOf(this, ChallengeTxnIncorrectSequenceError.prototype);
}
}

export class ChallengeTxnInvalidSignatureError extends Error {
constructor() {
super("Invalid signature for challenge transaction");
Object.setPrototypeOf(this, ChallengeTxnInvalidSignatureError.prototype);
}
}
60 changes: 60 additions & 0 deletions src/walletSdk/Server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Code in the Server module is written to be used by server side
* applications.
*/

import {
Transaction,
TransactionBuilder,
Keypair,
StellarToml,
} from "@stellar/stellar-sdk";

import { parseToml } from "../Utils";
import { SignChallengeTxnParams, SignChallengeTxnResponse } from "../Types";
import {
ChallengeTxnIncorrectSequenceError,
ChallengeTxnInvalidSignatureError,
} from "../Exceptions";

/**
* Helper method for signing a SEP-10 challenge transaction if valid.
* @param {SignChallengeTxnParams} params - The Authentication params.
* @param {AccountKeypair} params.accountKp - Keypair for the Stellar account signing the transaction.
* @param {string} [params.challengeTx] - The challenge transaction given by an anchor for authentication.
* @param {string} [params.networkPassphrase] - The network passphrase for the network authenticating on.
* @param {string} [params.anchorDomain] - Domain hosting stellar.toml file containing `SIGNING_KEY`.
* @returns {Promise<SignChallengeTxnResponse>} The signed transaction.
*/
export const signChallengeTransaction = async ({
accountKp,
challengeTx,
networkPassphrase,
anchorDomain,
}: SignChallengeTxnParams): Promise<SignChallengeTxnResponse> => {
const tx = TransactionBuilder.fromXDR(
challengeTx,
networkPassphrase,
) as Transaction;

if (parseInt(tx.sequence) !== 0) {
throw new ChallengeTxnIncorrectSequenceError();
}

const tomlResp = await StellarToml.Resolver.resolve(anchorDomain);
const parsedToml = parseToml(tomlResp);
const anchorKp = Keypair.fromPublicKey(parsedToml.signingKey);

const isValid =
tx.signatures.length &&
anchorKp.verify(tx.hash(), tx.signatures[0].signature());
if (!isValid) {
throw new ChallengeTxnInvalidSignatureError();
}

accountKp.sign(tx);
return {
transaction: tx.toXDR(),
networkPassphrase,
};
};
14 changes: 13 additions & 1 deletion src/walletSdk/Types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Transaction } from "@stellar/stellar-sdk";
import { decode } from "jws";

import { WalletSigner } from "../Auth/WalletSigner";
import { AccountKeypair } from "../Horizon/Account";
import { AccountKeypair, SigningKeypair } from "../Horizon/Account";

export type AuthenticateParams = {
accountKp: AccountKeypair;
Expand Down Expand Up @@ -79,3 +79,15 @@ export type SignWithDomainAccountParams = {
export type HttpHeaders = {
[key: string]: string;
};

export type SignChallengeTxnParams = {
accountKp: SigningKeypair;
challengeTx: string;
networkPassphrase: string;
anchorDomain: string;
};

export type SignChallengeTxnResponse = {
transaction: XdrEncodedTransaction;
networkPassphrase: NetworkPassphrase;
};
2 changes: 2 additions & 0 deletions src/walletSdk/Types/sep24.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type Sep24PostParams = {
destinationAccount?: string;
withdrawalAccount?: string;
account?: string;
callback?: string;
on_change_callback?: string;
};

export enum Sep24ResponseType {
Expand Down
52 changes: 12 additions & 40 deletions test/accountService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,50 +29,22 @@ describe("Horizon", () => {
);
}

const testingAccountKp = SigningKeypair.fromSecret(
"SAXW2HC7JH5IJSIRFQ22JTMT6T3VONKGMYSIBLHNEJCV7AXLIGAXNESD",
// create test account
const testingAccountKp = accountService.createKeypair();
await axios.get(
"https://friendbot.stellar.org/?addr=" + testingAccountKp.publicKey,
);

// make sure testing account exists
accountAddress = testingAccountKp.publicKey;
try {
await stellar.server.loadAccount(accountAddress);
} catch (e) {
const txBuilder1 = await stellar.transaction({
sourceAddress: fundingAccountKp,
baseFee: 100,
});
const createAccTx = txBuilder1.createAccount(testingAccountKp, 2).build();
createAccTx.sign(fundingAccountKp.keypair);

let failed = false;
try {
await stellar.submitTransaction(createAccTx);
await stellar.server.loadAccount(accountAddress);
} catch (e) {
failed = true;
}

const txBuilder2 = await stellar.transaction({
sourceAddress: testingAccountKp,
baseFee: 100,
});
const asset = new IssuedAssetId(
"USDC",
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
);
const addUsdcTx = txBuilder2.addAssetSupport(asset).build();
addUsdcTx.sign(testingAccountKp.keypair);
const txBuilder = await stellar.transaction({
sourceAddress: testingAccountKp,
});
const asset = new IssuedAssetId("USDC", fundingAccountKp.publicKey);
const addUsdcTx = txBuilder.addAssetSupport(asset).build();
addUsdcTx.sign(testingAccountKp.keypair);

// make sure testing account has USDC trustline
try {
await stellar.submitTransaction(addUsdcTx);
} catch (e) {
failed = true;
}
await stellar.submitTransaction(addUsdcTx);

expect(failed).toBeFalsy();
}
accountAddress = testingAccountKp.publicKey;
}, 30000);

it("should return stellar account details", async () => {
Expand Down
59 changes: 59 additions & 0 deletions test/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { TransactionBuilder } from "@stellar/stellar-sdk";
import { Wallet, Server } from "../src";

let wallet;
let account;
let accountKp;
const networkPassphrase = "Test SDF Network ; September 2015";
describe("SEP-10 helpers", () => {
beforeEach(() => {
wallet = Wallet.TestNet();
account = wallet.stellar().account();
accountKp = account.createKeypair();
});

it("should validate and sign challenge txn", async () => {
const validChallengeTx =
"AAAAAgAAAACpn2Fr7GAZ4XOcFvEz+xduBFDK1NDLQP875GtWWlJ0XQAAAMgAAAAAAAAAAAAAAAEAAAAAZa76AgAAAABlrv2GAAAAAAAAAAIAAAABAAAAALO9GbK9e+E+ul46lJyGjkzjlQnwqNryiqBsIR1vgMlAAAAACgAAABt0ZXN0YW5jaG9yLnN0ZWxsYXIub3JnIGF1dGgAAAAAAQAAAEBRT0ZDTE02OFQ0cVF4Um55TCtRdlBlVTdPeDJYNnhLdzdyenZTbzBBYUdqdUtIdGxQRkpHNTFKMndJazBwMXl2AAAAAQAAAACpn2Fr7GAZ4XOcFvEz+xduBFDK1NDLQP875GtWWlJ0XQAAAAoAAAAPd2ViX2F1dGhfZG9tYWluAAAAAAEAAAAWdGVzdGFuY2hvci5zdGVsbGFyLm9yZwAAAAAAAAAAAAFaUnRdAAAAQG6cMkt4YhwOzgizIimXRX8zTfFjAOItG7kSX14A454KlhGj9ocFhaRpj3tCc4fK45toFCBKRAdyFM7aQq331QI=";

let isValid;
try {
const signedResp = await Server.signChallengeTransaction({
accountKp,
challengeTx: validChallengeTx,
networkPassphrase,
anchorDomain: "testanchor.stellar.org",
});
const signedTxn = TransactionBuilder.fromXDR(
signedResp.transaction,
networkPassphrase,
);
expect(signedTxn.signatures.length).toBe(2);
expect(signedResp.networkPassphrase).toBe(networkPassphrase);
isValid = true;
} catch (e) {
isValid = false;
}

expect(isValid).toBeTruthy();
});

it("should invalidate bad challenge txn", async () => {
const invalidChallengeTx =
"AAAAAgAAAABQ5qHpn3ATIgt6yWrU4bhOdEszALPqLHb5V2pTRsYq0QAAAGQAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACgAAABZ0ZXN0YW5jaG9yLnN0ZWxsYXIub3JnAAAAAAABAAAAQFFPRkNMTTY4VDRxUXhSbnlMK1F2UGVVN094Mlg2eEt3N3J6dlNvMEFhR2p1S0h0bFBGSkc1MUoyd0lrMHAxeXYAAAAAAAAAAA==";

let isValid;
try {
await Server.signChallengeTransaction({
accountKp,
challengeTx: invalidChallengeTx,
networkPassphrase,
anchorDomain: "testanchor.stellar.org",
});
isValid = true;
} catch (e) {
isValid = false;
}
expect(isValid).toBeFalsy();
});
});
22 changes: 22 additions & 0 deletions test/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,28 @@ describe("Anchor", () => {
expect(resp.id).toBeTruthy();
});

it("should give url with a callback", async () => {
const assetCode = "SRT";
const resp = await anchor.sep24().deposit({
destinationAccount: accountKp.publicKey,
assetCode,
authToken,
callback: "mycallback.com",
});
expect(resp.url.includes("callback=mycallback.com")).toBeTruthy();
});

it("should give url with a on_change_callback", async () => {
const assetCode = "SRT";
const resp = await anchor.sep24().deposit({
destinationAccount: accountKp.publicKey,
assetCode,
authToken,
on_change_callback: "mycallback.com",
});
expect(resp.url.includes("on_change_callback=mycallback.com")).toBeTruthy();
});

it("should give interactive deposit url without giving account and giving memo", async () => {
const assetCode = "SRT";
const resp = await anchor.sep24().deposit({
Expand Down
Loading