From 7edba13893c7b6e1537396d2d9c7cb9b1fe38557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Marcos=20Goulart?= <3228151+CassioMG@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:32:26 -0300 Subject: [PATCH] [Sep-24] Add tests for `pending_`, `completed` and `stellar_transaction_id` (#117) * [Sep-24] Add tests for 'pending_' and 'completed' transaction * [Sep-24] Add tests for 'stellar_transaction_id' fetching * Add fields descriptions * Clean up * transaction.id should always be a 'string' * Bump version number * Bump to version 0.6.0 instead * More clarity on transaction schema definition * Move schemas around * Make it clear Sep-24 config file is optional * Only display the 'Upload Customer Files' button when it's needed * Fix 'accordian => accordion' typo * Display proper error msg when Sep-24 config object is missing * Rename Sep-24 account params and make publicKey optional --- @stellar/anchor-tests/package.json | 2 +- @stellar/anchor-tests/src/cli.ts | 2 +- @stellar/anchor-tests/src/helpers/sep10.ts | 8 +- @stellar/anchor-tests/src/helpers/sep24.ts | 75 +++ @stellar/anchor-tests/src/schemas/config.ts | 28 + @stellar/anchor-tests/src/schemas/sep24.ts | 425 +++++++++---- .../anchor-tests/src/tests/sep10/tests.ts | 54 +- .../anchor-tests/src/tests/sep24/deposit.ts | 8 +- .../src/tests/sep24/transaction.ts | 593 ++++++++++++++++-- .../src/tests/sep24/transactions.ts | 11 +- .../anchor-tests/src/tests/sep24/withdraw.ts | 8 +- @stellar/anchor-tests/src/types.ts | 87 ++- server/package.json | 2 +- .../components/ConfigModalContent/index.tsx | 132 +++- ui/src/components/TestCases/index.tsx | 12 +- ui/src/views/TestRunner.tsx | 140 +++-- 16 files changed, 1309 insertions(+), 278 deletions(-) create mode 100644 @stellar/anchor-tests/src/helpers/sep24.ts diff --git a/@stellar/anchor-tests/package.json b/@stellar/anchor-tests/package.json index d531077..e826b0a 100644 --- a/@stellar/anchor-tests/package.json +++ b/@stellar/anchor-tests/package.json @@ -1,6 +1,6 @@ { "name": "@stellar/anchor-tests", - "version": "0.5.12", + "version": "0.6.0", "description": "stellar-anchor-tests is a library and command line interface for testing Stellar anchors.", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/@stellar/anchor-tests/src/cli.ts b/@stellar/anchor-tests/src/cli.ts index 20d5b18..9f089d9 100644 --- a/@stellar/anchor-tests/src/cli.ts +++ b/@stellar/anchor-tests/src/cli.ts @@ -46,7 +46,7 @@ const command = yargs type: "string", requiresArg: true, description: - "A relative or absolute file path to JSON file containing the configuration required for SEP 6, 12, & 31.", + "A relative or absolute file path to JSON file containing the configuration used by SEP 6, 12, 24, 31 & 38.", }, }) .check((argv: any) => { diff --git a/@stellar/anchor-tests/src/helpers/sep10.ts b/@stellar/anchor-tests/src/helpers/sep10.ts index c1aff07..f9a9e5f 100644 --- a/@stellar/anchor-tests/src/helpers/sep10.ts +++ b/@stellar/anchor-tests/src/helpers/sep10.ts @@ -218,7 +218,7 @@ export const postChallengeFailureModes: Record = { }; export async function getChallenge( - clientKeypair: Keypair, + accountAddress: string, webAuthEndpoint: string, networkPassphrase: string, result: Result, @@ -228,9 +228,7 @@ export async function getChallenge( return; } const getAuthCall: NetworkCall = { - request: new Request( - webAuthEndpoint + `?account=${clientKeypair.publicKey()}`, - ), + request: new Request(webAuthEndpoint + `?account=${accountAddress}`), }; result.networkCalls.push(getAuthCall); try { @@ -301,7 +299,7 @@ export async function postChallenge( ): Promise { if (!challenge) { challenge = (await getChallenge( - clientKeypair, + clientKeypair.publicKey(), webAuthEndpoint, networkPassphrase, result, diff --git a/@stellar/anchor-tests/src/helpers/sep24.ts b/@stellar/anchor-tests/src/helpers/sep24.ts new file mode 100644 index 0000000..f92636b --- /dev/null +++ b/@stellar/anchor-tests/src/helpers/sep24.ts @@ -0,0 +1,75 @@ +import { Request } from "node-fetch"; + +import { Result, NetworkCall, Failure } from "../types"; +import { makeRequest } from "./request"; + +export const invalidTransactionSchema: Failure = { + name: "invalid transaction schema", + text(args: any): string { + return ( + "The response body returned does not comply with the schema defined for the /transaction endpoint. " + + "The errors returned from the schema validation:\n\n" + + `${args.errors}.` + ); + }, + links: { + "Transaction Schema": + "https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#single-historical-transaction", + }, +}; + +export const unexpectedTransactionStatus: Failure = { + name: "unexpected transaction status", + text(args: any): string { + return `Unexpected transaction status. Expected '${args.expected}' but received '${args.received}' instead.`; + }, +}; + +export const missingConfigFile: Failure = { + name: "missing config file", + text(args: any): string { + return `The ${args.sep} configuration object is missing. Please make sure to upload a config file containing a ${args.sep} configuration object in order for this test to run.`; + }, +}; + +export const invalidConfigFile: Failure = { + name: "invalid config file", + text(args: any): string { + return "The UPLOAD CONFIG file has some issues:\n\n" + `${args.errors}.`; + }, +}; + +export const fetchTransaction = async ({ + transferServerUrl, + transactionId, + stellarTransactionId, + authToken, + result, +}: { + transferServerUrl: string; + transactionId?: string; + stellarTransactionId?: string; + authToken: string; + result: Result; +}) => { + const idQuery = stellarTransactionId + ? `stellar_transaction_id=${stellarTransactionId}` + : `id=${transactionId}`; + + const getTransactionCall: NetworkCall = { + request: new Request(transferServerUrl + `/transaction?${idQuery}`, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }), + }; + result.networkCalls.push(getTransactionCall); + const response = await makeRequest( + getTransactionCall, + 200, + result, + "application/json", + ); + + return response; +}; diff --git a/@stellar/anchor-tests/src/schemas/config.ts b/@stellar/anchor-tests/src/schemas/config.ts index 096ee80..4255921 100644 --- a/@stellar/anchor-tests/src/schemas/config.ts +++ b/@stellar/anchor-tests/src/schemas/config.ts @@ -26,6 +26,33 @@ export const sep12ConfigSchema = { required: ["customers", "createCustomer", "deleteCustomer"], }; +export const sep24ConfigSchema = { + type: "object", + properties: { + account: { + type: "object", + minProperties: 1, + }, + depositPendingTransaction: { + type: "object", + minProperties: 2, + }, + depositCompletedTransaction: { + type: "object", + minProperties: 3, + }, + withdrawPendingUserTransferStartTransaction: { + type: "object", + minProperties: 7, + }, + withdrawCompletedTransaction: { + type: "object", + minProperties: 3, + }, + }, + required: [], +}; + export const sep31ConfigSchema = { type: "object", properties: { @@ -108,6 +135,7 @@ export const sepConfigSchema = { properties: { "6": sep6ConfigSchema, "12": sep12ConfigSchema, + "24": sep24ConfigSchema, "31": sep31ConfigSchema, "38": sep38ConfigSchema, }, diff --git a/@stellar/anchor-tests/src/schemas/sep24.ts b/@stellar/anchor-tests/src/schemas/sep24.ts index 919fdf0..e1a4d49 100644 --- a/@stellar/anchor-tests/src/schemas/sep24.ts +++ b/@stellar/anchor-tests/src/schemas/sep24.ts @@ -1,58 +1,5 @@ -const depositAndWithdrawInfoSchema = { - type: "object", - patternProperties: { - ".*": { - properties: { - enabled: { type: "boolean" }, - fee_fixed: { type: "number" }, - fee_minimum: { type: "number" }, - fee_percent: { type: "number" }, - min_amount: { type: "number" }, - max_amount: { type: "number" }, - }, - required: ["enabled"], - additionalProperties: false, - }, - }, -}; - -export const infoSchema = { - type: "object", - properties: { - deposit: depositAndWithdrawInfoSchema, - withdraw: depositAndWithdrawInfoSchema, - fee: { - type: "object", - properties: { - enabled: { type: "boolean" }, - authentication_required: { type: "boolean" }, - }, - required: ["enabled"], - }, - }, - required: ["deposit", "withdraw", "fee"], -}; - -export const successResponseSchema = { - type: "object", - properties: { - type: { - type: "string", - pattern: "interactive_customer_info_needed", - }, - url: { - type: "string", - format: "uri", - }, - id: { - type: "string", - }, - }, - required: ["type", "url", "id"], - additionalProperties: false, -}; - -export const transactionSchema = { +// =========================== > TRANSACTION schema < =========================== // +const transactionSchema = { type: "object", properties: { transaction: { @@ -62,25 +9,52 @@ export const transactionSchema = { kind: { type: "string", pattern: "deposit|withdrawal" }, status: { type: "string", - pattern: - "completed|pending_external|pending_anchor|pending_stellar|pending_trust|pending_user|pending_user_transfer_start|incomplete|no_market|too_small|too_large|error", + enum: [ + "incomplete", + "pending_anchor", + "pending_external", + "pending_stellar", + "pending_trust", + "pending_user", + "pending_user_transfer_start", + "pending_user_transfer_complete", + "completed", + "refunded", + "expired", + "no_market", + "too_small", + "too_large", + "error", + ], + }, + status_eta: { + type: ["number", "null"], + }, + kyc_verified: { + type: ["boolean", "null"], }, more_info_url: { type: "string", format: "uri", }, - status_eta: { - type: ["number", "null"], - }, amount_in: { type: ["string", "null"], }, + amount_in_asset: { + type: ["string", "null"], + }, amount_out: { type: ["string", "null"], }, + amount_out_asset: { + type: ["string", "null"], + }, amount_fee: { type: ["string", "null"], }, + amount_fee_asset: { + type: ["string", "null"], + }, started_at: { type: "string", format: "date-time", @@ -89,6 +63,10 @@ export const transactionSchema = { type: ["string", "null"], format: "date-time", }, + updated_at: { + type: ["string", "null"], + format: "date-time", + }, stellar_transaction_id: { type: ["string", "null"], }, @@ -99,28 +77,85 @@ export const transactionSchema = { type: ["string", "null"], }, refunded: { - type: "boolean", - }, - claimable_balance_id: { - type: ["string", "null"], - }, - kyc_verified: { type: ["boolean", "null"], }, + refunds: { + type: ["object", "null"], + }, }, - required: [ - "id", - "kind", - "status", - "more_info_url", - "started_at", - "refunded", - ], + required: ["id", "kind", "status", "more_info_url", "started_at"], }, }, required: ["transaction"], }; +const depositProperties = { + deposit_memo: { + type: ["string", "null"], + }, + deposit_memo_type: { + type: ["string", "null"], + }, + from: { + type: ["string", "null"], + }, + to: { + type: "string", + }, + claimable_balance_id: { + type: ["string", "null"], + }, +}; + +const withdrawProperties = { + withdraw_anchor_account: { + type: ["string", "null"], + }, + withdraw_memo: { + type: ["string", "null"], + }, + withdraw_memo_type: { + type: ["string", "null"], + }, + from: { + type: "string", + }, + to: { + type: ["string", "null"], + }, +}; + +const requiredIncompleteDepositProperties = ["to"]; + +const requiredIncompleteWithdrawProperties = ["from"]; + +const requiredPendingDepositProperties = [ + "amount_in", + "amount_in_asset", + "amount_out", + "amount_out_asset", +].concat(requiredIncompleteDepositProperties); + +const requiredPendingWithdrawProperties = [ + "amount_in", + "amount_in_asset", + "amount_out", + "amount_out_asset", + "withdraw_memo", + "withdraw_memo_type", + "withdraw_anchor_account", +].concat(requiredIncompleteWithdrawProperties); + +const requiredCompletedDepositProperties = [ + "stellar_transaction_id", + "completed_at", +].concat(requiredPendingDepositProperties); + +const requiredCompletedWithdrawProperties = [ + "stellar_transaction_id", + "completed_at", +].concat(requiredPendingWithdrawProperties); + export const transactionsSchema = { type: "object", properties: { @@ -128,8 +163,9 @@ export const transactionsSchema = { type: "array", items: { anyOf: [ - getTransactionSchema(true).properties.transaction, - getTransactionSchema(false).properties.transaction, + getTransactionSchema("deposit", "incomplete").properties.transaction, + getTransactionSchema("withdrawal", "incomplete").properties + .transaction, ], }, }, @@ -137,52 +173,227 @@ export const transactionsSchema = { required: ["transactions"], }; -export function getTransactionSchema(isDeposit: boolean) { +export function getTransactionSchema( + kind: "deposit" | "withdrawal", + status: + | "incomplete" + | "pending_" + | "pending_user_transfer_start" + | "completed", +) { + transactionSchema.properties.transaction.properties.kind.pattern = kind; + const schema = JSON.parse(JSON.stringify(transactionSchema)); - const requiredDepositParams = ["to"]; - const requiredWithdrawParams = ["from"]; - const depositProperties = { - deposit_memo: { - type: ["string", "null"], + let requiredProperties: string[] = []; + + if (kind === "deposit") { + if (status === "incomplete") { + requiredProperties = requiredIncompleteDepositProperties; + } else if (status === "pending_") { + requiredProperties = requiredPendingDepositProperties; + } else if (status === "completed") { + requiredProperties = requiredCompletedDepositProperties; + } else { + throw Error(`Unknown ${kind} transacion schema status: ${status}`); + } + + schema.properties.transaction.required = + schema.properties.transaction.required.concat(requiredProperties); + + Object.assign(schema.properties.transaction.properties, depositProperties); + } else if (kind === "withdrawal") { + if (status === "incomplete") { + requiredProperties = requiredIncompleteWithdrawProperties; + } else if (status === "pending_user_transfer_start") { + requiredProperties = requiredPendingWithdrawProperties; + } else if (status === "completed") { + requiredProperties = requiredCompletedWithdrawProperties; + } else { + throw Error(`Unknown ${kind} transacion schema status: ${status}`); + } + + schema.properties.transaction.required = + schema.properties.transaction.required.concat(requiredProperties); + + Object.assign(schema.properties.transaction.properties, withdrawProperties); + } else { + throw Error(`Unknown transacion schema kind: ${kind}`); + } + + return schema; +} + +// ============================= > GET INFO schema < ============================ // +const depositAndWithdrawInfoSchema = { + type: "object", + patternProperties: { + ".*": { + properties: { + enabled: { type: "boolean" }, + fee_fixed: { type: "number" }, + fee_minimum: { type: "number" }, + fee_percent: { type: "number" }, + min_amount: { type: "number" }, + max_amount: { type: "number" }, + }, + required: ["enabled"], + additionalProperties: false, }, - deposit_memo_type: { - type: ["string", "null"], + }, +}; + +export const infoSchema = { + type: "object", + properties: { + deposit: depositAndWithdrawInfoSchema, + withdraw: depositAndWithdrawInfoSchema, + fee: { + type: "object", + properties: { + enabled: { type: "boolean" }, + authentication_required: { type: "boolean" }, + }, + required: ["enabled"], }, - from: { - type: ["string", "null"], + }, + required: ["deposit", "withdraw", "fee"], +}; + +// ======================= > INTERACTIVE RESPONSE schema < ====================== // +export const successResponseSchema = { + type: "object", + properties: { + type: { + type: "string", + pattern: "interactive_customer_info_needed", + }, + url: { + type: "string", + format: "uri", }, - to: { - type: ["string", "null"], + id: { + type: "string", }, - }; + }, + required: ["type", "url", "id"], + additionalProperties: false, +}; - const withdrawProperties = { - withdraw_anchor_account: { - type: ["string", "null"], +// =========================== > CONFIG FILE schema < =========================== // +const initialConfigSchema = { + type: "object", + properties: { + account: { + type: "object", + properties: { + publicKey: { type: "string" }, + secretKey: { type: "string" }, + }, + required: ["secretKey"], }, - withdraw_memo: { - type: ["string", "null"], + }, + required: ["account"], +}; + +const pendingDepositConfigSchema = { + depositPendingTransaction: { + type: "object", + properties: { + status: { + type: "string", + enum: [ + "pending_anchor", + "pending_external", + "pending_stellar", + "pending_trust", + "pending_user", + "pending_user_transfer_start", + "pending_user_transfer_complete", + ], + }, + id: { type: "string" }, }, - withdraw_memo_type: { - type: ["string", "null"], + required: ["status", "id"], + }, +}; + +const completedDepositConfigSchema = { + depositCompletedTransaction: { + type: "object", + properties: { + status: { + type: "string", + pattern: "completed", + }, + id: { type: "string" }, + stellar_transaction_id: { type: "string" }, }, - from: { - type: ["string", "null"], + required: ["status", "id", "stellar_transaction_id"], + }, +}; + +const pendingWithdrawConfigSchema = { + withdrawPendingUserTransferStartTransaction: { + type: "object", + properties: { + status: { + type: "string", + pattern: "pending_user_transfer_start", + }, + id: { type: "string" }, + amount_in: { type: "string" }, + amount_in_asset: { type: "string" }, + withdraw_anchor_account: { type: "string" }, + withdraw_memo: { type: "string" }, + withdraw_memo_type: { type: "string" }, }, - to: { - type: ["string", "null"], + required: [ + "status", + "id", + "amount_in", + "amount_in_asset", + "withdraw_anchor_account", + ], + }, +}; + +const completedWithdrawConfigSchema = { + withdrawCompletedTransaction: { + type: "object", + properties: { + status: { + type: "string", + pattern: "completed", + }, + id: { type: "string" }, + stellar_transaction_id: { type: "string" }, }, - }; + required: ["status", "id", "stellar_transaction_id"], + }, +}; + +export function getConfigFileSchema(isDeposit: boolean, isPending: boolean) { + const schema = JSON.parse(JSON.stringify(initialConfigSchema)); if (isDeposit) { - schema.properties.transaction.required = - schema.properties.transaction.required.concat(requiredDepositParams); - Object.assign(schema.properties.transaction.properties, depositProperties); + if (isPending) { + schema.required = schema.required.concat("depositPendingTransaction"); + Object.assign(schema.properties, pendingDepositConfigSchema); + } else { + schema.required = schema.required.concat("depositCompletedTransaction"); + Object.assign(schema.properties, completedDepositConfigSchema); + } } else { - schema.properties.transaction.required = - schema.properties.transaction.required.concat(requiredWithdrawParams); - Object.assign(schema.properties.transaction.properties, withdrawProperties); + if (isPending) { + schema.required = schema.required.concat( + "withdrawPendingUserTransferStartTransaction", + ); + Object.assign(schema.properties, pendingWithdrawConfigSchema); + } else { + schema.required = schema.required.concat("withdrawCompletedTransaction"); + Object.assign(schema.properties, completedWithdrawConfigSchema); + } } return schema; diff --git a/@stellar/anchor-tests/src/tests/sep10/tests.ts b/@stellar/anchor-tests/src/tests/sep10/tests.ts index c3ab52d..7c828c7 100644 --- a/@stellar/anchor-tests/src/tests/sep10/tests.ts +++ b/@stellar/anchor-tests/src/tests/sep10/tests.ts @@ -731,21 +731,53 @@ export const returnsValidJwt: Test = { async run(config: Config): Promise { const result: Result = { networkCalls: [] }; this.context.provides.clientKeypair = Keypair.random(); - if ( - config.sepConfig && - config.sepConfig["31"] && - config.sepConfig["31"].sendingAnchorClientSecret - ) { + + const sep24AccountSigner = config.sepConfig?.["24"]?.account?.secretKey; + if (sep24AccountSigner) { + const signerKeypair = Keypair.fromSecret(sep24AccountSigner); + + // If the optional 'publicKey' value is provided let's use that for the + // 'clientKeypair', otherwise let's infer the publicKey value from the + // provided 'secretKey' + const sep24AccountAddress = config.sepConfig?.["24"]?.account?.publicKey; + this.context.provides.clientKeypair = sep24AccountAddress + ? Keypair.fromPublicKey(sep24AccountAddress) + : signerKeypair; + + const challenge = (await getChallenge( + this.context.provides.clientKeypair.publicKey(), + this.context.expects.webAuthEndpoint, + this.context.expects.tomlObj.NETWORK_PASSPHRASE, + result, + )) as Transaction; + + challenge.sign(signerKeypair); + + this.context.provides.token = await postChallenge( + this.context.provides.clientKeypair, + this.context.expects.webAuthEndpoint, + this.context.expects.tomlObj.NETWORK_PASSPHRASE, + result, + false, + challenge, + ); + + return result; + } + + if (config?.sepConfig?.["31"]?.sendingAnchorClientSecret) { this.context.provides.clientKeypair = Keypair.fromSecret( config.sepConfig["31"].sendingAnchorClientSecret, ); } + this.context.provides.token = await postChallenge( this.context.provides.clientKeypair, this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, ); + return result; }, }; @@ -880,7 +912,7 @@ const failsWithNoClientSignature: Test = { const result: Result = { networkCalls: [] }; const clientKeypair = Keypair.random(); const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, @@ -1015,7 +1047,7 @@ const extraClientSigners: Test = { const result: Result = { networkCalls: [] }; const clientKeypair = Keypair.random(); const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, @@ -1097,7 +1129,7 @@ const failsIfWeighBelowMediumThreshold: Test = { ); if (!horizonResponse) return result; const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, @@ -1178,7 +1210,7 @@ const signedByNonMasterSigner: Test = { await submitTransaction(raiseThresholdsTx.toXDR(), result); if (result.failure) return result; const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, @@ -1265,7 +1297,7 @@ const failsWithDuplicateSignatures: Test = { await submitTransaction(raiseThresholdsTx.toXDR(), result); if (result.failure) return result; const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, @@ -1358,7 +1390,7 @@ const multipleNonMasterSigners: Test = { await submitTransaction(raiseThresholdsTx.toXDR(), result); if (result.failure) return result; const challenge = await getChallenge( - clientKeypair, + clientKeypair.publicKey(), this.context.expects.webAuthEndpoint, this.context.expects.tomlObj.NETWORK_PASSPHRASE, result, diff --git a/@stellar/anchor-tests/src/tests/sep24/deposit.ts b/@stellar/anchor-tests/src/tests/sep24/deposit.ts index d3df259..0cf0691 100644 --- a/@stellar/anchor-tests/src/tests/sep24/deposit.ts +++ b/@stellar/anchor-tests/src/tests/sep24/deposit.ts @@ -31,7 +31,7 @@ export const invalidDepositSchema: Failure = { }; const depositRequiresToken: Test = { - assertion: "requires a SEP-10 JWT", + assertion: "requires a SEP-10 JWT for deposit", sep: 24, group: depositTestsGroup, dependencies: [hasTransferServerUrl, assetCodeEnabledForDeposit], @@ -69,7 +69,7 @@ const depositRequiresToken: Test = { tests.push(depositRequiresToken); const depositRequiresAssetCode: Test = { - assertion: "requires 'asset_code' parameter", + assertion: "requires 'asset_code' parameter for deposit", sep: 24, group: depositTestsGroup, dependencies: [ @@ -184,7 +184,7 @@ const depositRejectsInvalidAccount: Test = { tests.push(depositRejectsInvalidAccount); const depositRejectsUnsupportedAssetCode: Test = { - assertion: "rejects unsupported 'asset_code' parameter", + assertion: "rejects unsupported 'asset_code' parameter for deposit", sep: 24, group: depositTestsGroup, dependencies: depositRequiresAssetCode.dependencies, @@ -227,7 +227,7 @@ const depositRejectsUnsupportedAssetCode: Test = { tests.push(depositRejectsUnsupportedAssetCode); export const returnsProperSchemaForValidDepositRequest: Test = { - assertion: "returns a proper schema for valid requests", + assertion: "returns a proper schema for valid deposit requests", sep: 24, group: depositTestsGroup, dependencies: depositRequiresAssetCode.dependencies, diff --git a/@stellar/anchor-tests/src/tests/sep24/transaction.ts b/@stellar/anchor-tests/src/tests/sep24/transaction.ts index 7d4e4c1..def8573 100644 --- a/@stellar/anchor-tests/src/tests/sep24/transaction.ts +++ b/@stellar/anchor-tests/src/tests/sep24/transaction.ts @@ -1,10 +1,17 @@ import { Request } from "node-fetch"; import { validate } from "jsonschema"; -import { Test, Config, Result, NetworkCall, Failure } from "../../types"; -import { getTransactionSchema } from "../../schemas/sep24"; +import { Test, Config, Result, NetworkCall } from "../../types"; +import { getConfigFileSchema, getTransactionSchema } from "../../schemas/sep24"; import { makeRequest } from "../../helpers/request"; import { genericFailures, makeFailure } from "../../helpers/failure"; +import { + fetchTransaction, + missingConfigFile, + invalidConfigFile, + invalidTransactionSchema, + unexpectedTransactionStatus, +} from "../../helpers/sep24"; import { hasTransferServerUrl } from "./toml"; import { returnsValidJwt } from "../sep10/tests"; import { returnsProperSchemaForValidDepositRequest } from "./deposit"; @@ -14,23 +21,8 @@ const transactionEndpoint = "/transaction"; const transactionTestGroup = "/transaction"; const tests: Test[] = []; -const invalidTransactionSchema: Failure = { - name: "invalid schema", - text(args: any): string { - return ( - "The response body returned does not comply with the schema defined for the /transaction endpoint. " + - "The errors returned from the schema validation:\n\n" + - `${args.errors}` - ); - }, - links: { - "Transaction Schema": - "https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#single-historical-transaction", - }, -}; - const transactionRequiresToken: Test = { - assertion: "requires a JWT", + assertion: "requires a SEP-10 JWT on /transaction", sep: 24, group: transactionTestGroup, dependencies: [hasTransferServerUrl, returnsValidJwt], @@ -77,25 +69,14 @@ const transactionIsPresentAfterDepositRequest: Test = { failureModes: genericFailures, async run(_config: Config): Promise { const result: Result = { networkCalls: [] }; - const getTransactionCall: NetworkCall = { - request: new Request( - this.context.expects.transferServerUrl + - transactionEndpoint + - `?id=${this.context.expects.depositTransactionId}`, - { - headers: { - Authorization: `Bearer ${this.context.expects.token}`, - }, - }, - ), - }; - result.networkCalls.push(getTransactionCall); - this.context.provides.depositTransactionObj = await makeRequest( - getTransactionCall, - 200, + + this.context.provides.depositTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: this.context.expects.depositTransactionId, + authToken: this.context.expects.token, result, - "application/json", - ); + }); + return result; }, }; @@ -123,32 +104,22 @@ const transactionIsPresentAfterWithdrawRequest: Test = { failureModes: genericFailures, async run(_config: Config): Promise { const result: Result = { networkCalls: [] }; - const getTransactionCall: NetworkCall = { - request: new Request( - this.context.expects.transferServerUrl + - transactionEndpoint + - `?id=${this.context.expects.withdrawTransactionId}`, - { - headers: { - Authorization: `Bearer ${this.context.expects.token}`, - }, - }, - ), - }; - result.networkCalls.push(getTransactionCall); - this.context.provides.withdrawTransactionObj = await makeRequest( - getTransactionCall, - 200, + + this.context.provides.withdrawTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: this.context.expects.withdrawTransactionId, + authToken: this.context.expects.token, result, - "application/json", - ); + }); + return result; }, }; tests.push(transactionIsPresentAfterWithdrawRequest); -const hasProperDepositTransactionSchema: Test = { - assertion: "has proper deposit transaction schema on /transaction", +export const hasProperIncompleteDepositTransactionSchema: Test = { + assertion: + "has proper 'incomplete' deposit transaction schema on /transaction", sep: 24, group: transactionTestGroup, dependencies: [ @@ -164,26 +135,201 @@ const hasProperDepositTransactionSchema: Test = { }, failureModes: { INVALID_SCHEMA: invalidTransactionSchema, + UNEXPECTED_STATUS: unexpectedTransactionStatus, ...genericFailures, }, async run(_config: Config): Promise { const result: Result = { networkCalls: [] }; const validationResult = validate( this.context.expects.depositTransactionObj, - getTransactionSchema(true), + getTransactionSchema("deposit", "incomplete"), ); if (validationResult.errors.length !== 0) { result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { errors: validationResult.errors.join("\n"), }); + + return result; + } + + const transactionStatus = + this.context.expects.depositTransactionObj.transaction.status; + if (transactionStatus !== "incomplete") { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "incomplete", + received: transactionStatus, + }); } return result; }, }; -tests.push(hasProperDepositTransactionSchema); +tests.push(hasProperIncompleteDepositTransactionSchema); -export const hasProperWithdrawTransactionSchema: Test = { - assertion: "has proper withdraw transaction schema on /transaction", +const hasProperPendingDepositTransactionSchema: Test = { + assertion: "has proper 'pending_' deposit transaction schema on /transaction", + sep: 24, + group: transactionTestGroup, + dependencies: [ + hasTransferServerUrl, + returnsValidJwt, + returnsProperSchemaForValidDepositRequest, + ], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_CONFIG: invalidConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + UNEXPECTED_STATUS: unexpectedTransactionStatus, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(true, true), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const pendingDepositTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: config.sepConfig["24"].depositPendingTransaction?.id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + pendingDepositTransactionObj, + getTransactionSchema("deposit", "pending_"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + + return result; + } + + const transactionStatus = pendingDepositTransactionObj.transaction + .status as string; + if (!transactionStatus.startsWith("pending_")) { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "any of pending_", + received: transactionStatus, + }); + } + return result; + }, +}; +tests.push(hasProperPendingDepositTransactionSchema); + +const hasProperCompletedDepositTransactionSchema: Test = { + assertion: + "has proper 'completed' deposit transaction schema on /transaction", + sep: 24, + group: transactionTestGroup, + dependencies: [ + hasTransferServerUrl, + returnsValidJwt, + returnsProperSchemaForValidDepositRequest, + ], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + INVALID_CONFIG: invalidConfigFile, + UNEXPECTED_STATUS: unexpectedTransactionStatus, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(true, false), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const completedDepositTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: config.sepConfig["24"].depositCompletedTransaction?.id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + completedDepositTransactionObj, + getTransactionSchema("deposit", "completed"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + + return result; + } + + const transactionStatus = completedDepositTransactionObj.transaction.status; + if (transactionStatus !== "completed") { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "completed", + received: transactionStatus, + }); + } + return result; + }, +}; +tests.push(hasProperCompletedDepositTransactionSchema); + +export const hasProperIncompleteWithdrawTransactionSchema: Test = { + assertion: + "has proper 'incomplete' withdraw transaction schema on /transaction", sep: 24, group: transactionTestGroup, dependencies: [ @@ -199,29 +345,344 @@ export const hasProperWithdrawTransactionSchema: Test = { }, failureModes: { INVALID_SCHEMA: invalidTransactionSchema, + UNEXPECTED_STATUS: unexpectedTransactionStatus, ...genericFailures, }, async run(_config: Config): Promise { const result: Result = { networkCalls: [] }; const validationResult = validate( this.context.expects.withdrawTransactionObj, - getTransactionSchema(false), + getTransactionSchema("withdrawal", "incomplete"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + + return result; + } + + const transactionStatus = + this.context.expects.withdrawTransactionObj.transaction.status; + if (transactionStatus !== "incomplete") { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "incomplete", + received: transactionStatus, + }); + } + + return result; + }, +}; +tests.push(hasProperIncompleteWithdrawTransactionSchema); + +export const hasProperPendingWithdrawTransactionSchema: Test = { + assertion: + "has proper 'pending_user_transfer_start' withdraw transaction schema on /transaction", + sep: 24, + group: transactionTestGroup, + dependencies: [ + hasTransferServerUrl, + returnsValidJwt, + returnsProperSchemaForValidWithdrawRequest, + ], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_CONFIG: invalidConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + UNEXPECTED_STATUS: unexpectedTransactionStatus, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(false, true), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const pendingWithdrawTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: + config.sepConfig["24"].withdrawPendingUserTransferStartTransaction?.id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + pendingWithdrawTransactionObj, + getTransactionSchema("withdrawal", "pending_user_transfer_start"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + + return result; + } + + const transactionStatus = pendingWithdrawTransactionObj.transaction.status; + if (transactionStatus !== "pending_user_transfer_start") { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "pending_user_transfer_start", + received: transactionStatus, + }); + } + return result; + }, +}; +tests.push(hasProperPendingWithdrawTransactionSchema); + +export const hasProperCompletedWithdrawTransactionSchema: Test = { + assertion: + "has proper 'completed' withdraw transaction schema on /transaction", + sep: 24, + group: transactionTestGroup, + dependencies: [ + hasTransferServerUrl, + returnsValidJwt, + returnsProperSchemaForValidWithdrawRequest, + ], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_CONFIG: invalidConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + UNEXPECTED_STATUS: unexpectedTransactionStatus, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(false, false), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const completedWithdrawTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + transactionId: config.sepConfig["24"].withdrawCompletedTransaction?.id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + completedWithdrawTransactionObj, + getTransactionSchema("withdrawal", "completed"), ); if (validationResult.errors.length !== 0) { result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { errors: validationResult.errors.join("\n"), }); + + return result; } + + const transactionStatus = + completedWithdrawTransactionObj.transaction.status; + if (transactionStatus !== "completed") { + result.failure = makeFailure(this.failureModes.UNEXPECTED_STATUS, { + expected: "completed", + received: transactionStatus, + }); + } + return result; + }, +}; +tests.push(hasProperCompletedWithdrawTransactionSchema); + +const returnsDepositTransactionForStellarTxId: Test = { + assertion: + "returns valid deposit transaction when using 'stellar_transaction_id' param", + sep: 24, + group: transactionTestGroup, + dependencies: [hasTransferServerUrl, returnsValidJwt], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_CONFIG: invalidConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(true, false), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const fetchedDepositTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + stellarTransactionId: + config.sepConfig["24"].depositCompletedTransaction + ?.stellar_transaction_id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + fetchedDepositTransactionObj, + getTransactionSchema("deposit", "completed"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + } + + return result; + }, +}; +tests.push(returnsDepositTransactionForStellarTxId); + +const returnsWithdrawTransactionForStellarTxId: Test = { + assertion: + "returns valid withdraw transaction when using 'stellar_transaction_id' param", + sep: 24, + group: transactionTestGroup, + dependencies: [hasTransferServerUrl, returnsValidJwt], + context: { + expects: { + transferServerUrl: undefined, + token: undefined, + }, + provides: {}, + }, + failureModes: { + MISSING_CONFIG: missingConfigFile, + INVALID_CONFIG: invalidConfigFile, + INVALID_SCHEMA: invalidTransactionSchema, + ...genericFailures, + }, + async run(config: Config): Promise { + const result: Result = { networkCalls: [] }; + + if (!config.sepConfig?.["24"]) { + result.failure = makeFailure(this.failureModes.MISSING_CONFIG, { + sep: "SEP-24", + }); + return result; + } + + const configValidationResult = validate( + config.sepConfig["24"], + getConfigFileSchema(false, false), + ); + if (configValidationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_CONFIG, { + errors: configValidationResult.errors.join("; "), + }); + + return result; + } + + const fetchedWithdrawTransactionObj = await fetchTransaction({ + transferServerUrl: this.context.expects.transferServerUrl, + stellarTransactionId: + config.sepConfig["24"].withdrawCompletedTransaction + ?.stellar_transaction_id, + authToken: this.context.expects.token, + result, + }); + + if (result.failure) { + return result; + } + + const validationResult = validate( + fetchedWithdrawTransactionObj, + getTransactionSchema("withdrawal", "completed"), + ); + if (validationResult.errors.length !== 0) { + result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, { + errors: validationResult.errors.join("\n"), + }); + } + return result; }, }; -tests.push(hasProperWithdrawTransactionSchema); +tests.push(returnsWithdrawTransactionForStellarTxId); const hasValidMoreInfoUrl: Test = { assertion: "has a valid 'more_info_url'", sep: 24, group: transactionTestGroup, - dependencies: [hasProperDepositTransactionSchema], + dependencies: [hasProperIncompleteDepositTransactionSchema], context: { expects: { depositTransactionObj: undefined, @@ -243,7 +704,7 @@ const hasValidMoreInfoUrl: Test = { tests.push(hasValidMoreInfoUrl); const returns404ForBadId: Test = { - assertion: "returns 404 for a nonexistent transaction ID", + assertion: "returns 404 for a nonexistent transaction 'id'", sep: 24, group: transactionTestGroup, dependencies: [hasTransferServerUrl, returnsValidJwt], @@ -276,7 +737,7 @@ const returns404ForBadId: Test = { tests.push(returns404ForBadId); const returns404ForBadExternalId: Test = { - assertion: "returns 404 for a nonexistent external transaction ID", + assertion: "returns 404 for a nonexistent 'external_transaction_id'", sep: 24, group: transactionTestGroup, dependencies: [hasTransferServerUrl, returnsValidJwt], @@ -309,7 +770,7 @@ const returns404ForBadExternalId: Test = { tests.push(returns404ForBadExternalId); const returns404ForBadStellarId: Test = { - assertion: "returns 404 for a nonexistent Stellar transaction ID", + assertion: "returns 404 for a nonexistent 'stellar_transaction_id'", sep: 24, group: transactionTestGroup, dependencies: [hasTransferServerUrl, returnsValidJwt], diff --git a/@stellar/anchor-tests/src/tests/sep24/transactions.ts b/@stellar/anchor-tests/src/tests/sep24/transactions.ts index a6b6faf..0437023 100644 --- a/@stellar/anchor-tests/src/tests/sep24/transactions.ts +++ b/@stellar/anchor-tests/src/tests/sep24/transactions.ts @@ -20,7 +20,10 @@ import { depositEndpoint, } from "./deposit"; import { returnsProperSchemaForValidWithdrawRequest } from "./withdraw"; -import { hasProperWithdrawTransactionSchema } from "./transaction"; +import { + hasProperIncompleteDepositTransactionSchema, + hasProperIncompleteWithdrawTransactionSchema, +} from "./transaction"; const transactionsTestGroup = "/transactions"; const tests: Test[] = []; @@ -43,7 +46,7 @@ const invalidTransactionsSchema = { }; const transactionsRequiresToken: Test = { - assertion: "requires a JWT", + assertion: "requires a SEP-10 JWT on /transactions", sep: 24, group: transactionsTestGroup, dependencies: [hasTransferServerUrl, returnsValidJwt], @@ -845,7 +848,7 @@ const honorsWithdrawTransactionKind: Test = { dependencies: [ hasTransferServerUrl, hasProperDepositTransactionsSchema, - hasProperWithdrawTransactionSchema, + hasProperIncompleteWithdrawTransactionSchema, returnsValidJwt, ], context: { @@ -931,7 +934,7 @@ const honorsDepositTransactionKind: Test = { dependencies: [ hasTransferServerUrl, hasProperDepositTransactionsSchema, - hasProperWithdrawTransactionSchema, + hasProperIncompleteDepositTransactionSchema, returnsValidJwt, ], context: { diff --git a/@stellar/anchor-tests/src/tests/sep24/withdraw.ts b/@stellar/anchor-tests/src/tests/sep24/withdraw.ts index d19ad59..00927db 100644 --- a/@stellar/anchor-tests/src/tests/sep24/withdraw.ts +++ b/@stellar/anchor-tests/src/tests/sep24/withdraw.ts @@ -31,7 +31,7 @@ export const invalidWithdrawSchema: Failure = { }; const withdrawRequiresToken: Test = { - assertion: "requires a SEP-10 JWT", + assertion: "requires a SEP-10 JWT for withdraw", sep: 24, group: withdrawTestsGroup, dependencies: [hasTransferServerUrl, assetCodeEnabledForWithdraw], @@ -69,7 +69,7 @@ const withdrawRequiresToken: Test = { tests.push(withdrawRequiresToken); const withdrawRequiresAssetCode: Test = { - assertion: "requires 'asset_code' parameter", + assertion: "requires 'asset_code' parameter for withdraw", sep: 24, group: withdrawTestsGroup, dependencies: [ @@ -141,7 +141,7 @@ const withdrawRequiresAssetCode: Test = { tests.push(withdrawRequiresAssetCode); const withdrawRejectsUnsupportedAssetCode: Test = { - assertion: "rejects unsupported 'asset_code' parameter", + assertion: "rejects unsupported 'asset_code' parameter for withdraw", sep: 24, group: withdrawTestsGroup, dependencies: withdrawRequiresAssetCode.dependencies, @@ -184,7 +184,7 @@ const withdrawRejectsUnsupportedAssetCode: Test = { tests.push(withdrawRejectsUnsupportedAssetCode); export const returnsProperSchemaForValidWithdrawRequest: Test = { - assertion: "returns a proper schema for valid requests", + assertion: "returns a proper schema for valid withdraw requests", sep: 24, group: withdrawTestsGroup, dependencies: withdrawRequiresAssetCode.dependencies, diff --git a/@stellar/anchor-tests/src/types.ts b/@stellar/anchor-tests/src/types.ts index a257d18..939d0e8 100644 --- a/@stellar/anchor-tests/src/types.ts +++ b/@stellar/anchor-tests/src/types.ts @@ -44,7 +44,7 @@ export interface Config { searchStrings?: string[]; /** - * Only relevant for SEP-6, 12, 31 & 38. + * Only relevant for SEP-6, 12, 24, 31 & 38. * * A ``SepConfig`` object. */ @@ -62,6 +62,7 @@ export interface Config { export interface SepConfig { 6?: Sep6Config; 12?: Sep12Config; + 24?: Sep24Config; 31?: Sep31Config; 38?: Sep38Config; } @@ -106,6 +107,90 @@ export interface Sep12Config { sameAccountDifferentMemos?: [string, string]; } +/** + * The configuration object for SEP-24 tests. + */ +export interface Sep24Config { + /** + * The credentials that should be used to fetch the pending and completed transactions listed below + * (depositPendingTransaction, depositCompletedTransaction, withdrawPendingUserTransferStartTransaction + * and withdrawCompletedTransaction). + * + * - publicKey: (optional) the public key of the account which holds the pending and completed transactions. + * in case this param is not provided then the public key will be inferred from the secretKey below. + * - secretKey: the secret key of a valid signer on the account. + * + * When provided, it'll be used in the sep10 "returns a valid JWT" test. + */ + account: { + publicKey?: string; + secretKey: string; + }; + + /** + * Should reference the "id" of a "deposit" transaction which is in any of the "pending_" status. + * + * When provided, it'll be used in the sep24 + * "has proper 'pending_' deposit transaction schema on /transaction" test. + */ + depositPendingTransaction: { + id: string; + status: + | "pending_anchor" + | "pending_external" + | "pending_stellar" + | "pending_trust" + | "pending_user" + | "pending_user_transfer_start" + | "pending_user_transfer_complete"; + }; + + /** + * Should reference the "id" and "stellar_transaction_id" of a "deposit" transaction which is in a + * "completed" status. + * + * When provided, they'll be used in the following sep24 tests: + * - "has proper 'completed' deposit transaction schema on /transaction" + * - "returns valid deposit transaction when using 'stellar_transaction_id' param" + */ + depositCompletedTransaction: { + id: string; + status: "completed"; + stellar_transaction_id: string; + }; + + /** + * Should reference the "id" and other parameters of a "withdrawal" transaction which is + * in the "pending_user_transfer_start" status. + * + * When provided, it'll be used in the sep24 + * "has proper 'pending_user_transfer_start' withdraw transaction schema on /transaction" test. + */ + withdrawPendingUserTransferStartTransaction: { + id: string; + status: "pending_user_transfer_start"; + amount_in: string; + amount_in_asset: string; + withdraw_anchor_account: string; + withdraw_memo: string; + withdraw_memo_type: string; + }; + + /** + * Should reference the "id" and "stellar_transaction_id" of a "withdrawal" transaction which is in a + * "completed" status. + * + * When provided, they'll be used in the following sep24 tests: + * - "has proper 'completed' withdraw transaction schema on /transaction" + * - "returns valid withdraw transaction when using 'stellar_transaction_id' param" + */ + withdrawCompletedTransaction: { + id: string; + status: "completed"; + stellar_transaction_id: string; + }; +} + /** * The configuration object for SEP-31 tests. */ diff --git a/server/package.json b/server/package.json index 3591cee..85bc921 100644 --- a/server/package.json +++ b/server/package.json @@ -30,6 +30,6 @@ "winston": "^3.3.3" }, "peerDependencies": { - "@stellar/anchor-tests": "0.5.12" + "@stellar/anchor-tests": "0.6.0" } } diff --git a/ui/src/components/ConfigModalContent/index.tsx b/ui/src/components/ConfigModalContent/index.tsx index 27fd9eb..dd96a56 100644 --- a/ui/src/components/ConfigModalContent/index.tsx +++ b/ui/src/components/ConfigModalContent/index.tsx @@ -7,9 +7,9 @@ export const ConfigModalContent: React.FC = () => ( Configuration Files

- Configuration files are required for running SEP-6, 12, and 31 tests. - These files provide information required for the test suite to interact - with your anchor service properly. + Configuration files are required for running SEP-6, 12, 31 and 38 tests, + and is optional for SEP-24 tests. These files provide information needed + for the test suite to interact with your anchor service properly.

Configuration files are JSON-formatted and have top-level keys for each @@ -19,6 +19,7 @@ export const ConfigModalContent: React.FC = () => ( src={{ "6": {}, "12": {}, + "24": {}, "31": {}, "38": {}, }} @@ -129,7 +130,7 @@ export const ConfigModalContent: React.FC = () => ( SEP-9 {" "} - attributes the anchor requires, as well as the + attributes the anchor requires, as well as the{" "} type {" "} @@ -151,6 +152,129 @@ export const ConfigModalContent: React.FC = () => (

Each customers key assigned to the other attributes must be unique.

+ SEP-24 (optional*) +

+ SEP-24 configuration objects are used for specifying the account + credentials and transaction parameters that should be used when making + GET /transaction requests for fetching pending and completed + transactions. +

+

+ *This SEP-24 configuration object is optional so + Anchors are able to initially skip the tests for pending and completed + transactions. But we strongly encourage Anchors to + succeed with ALL tests in order to minimize issues before onboarding + with a new wallet. +

+ +
    +
  • + account: an object containing the credentials of the + account which holds all the pending and completed transactions (aka{" "} + depositPendingTransaction,{" "} + depositCompletedTransaction,{" "} + withdrawPendingUserTransferStartTransaction,{" "} + withdrawCompletedTransaction) that will be used to + test against. The account object must contain a{" "} + secretKey param which should be a valid signer on the + account. In case the publicKey param is not set it'll + be inferred from the provided secretKey. +
  • +
  • + depositPendingTransaction: this object should contain + an id of a deposit transaction which + is in any of the 'pending_' status usually achieved + after{" "} + + closing the interactive popup + + . +
  • +
  • + depositCompletedTransaction: this object should + contain an id of a deposit{" "} + transaction which is in a 'completed' status usually + achieved after the user receives the funds in the wallet. This object + should also contain a stellar_transaction_id{" "} + parameter which is the id of the stellar payment transaction used to + send funds from Anchor to wallet. The wallet should be able to fetch + the Sep-24 transaction object through the{" "} + + GET /transaction + {" "} + endpoint using this stellar_transaction_id as the + query parameter. +
  • +
  • + withdrawPendingUserTransferStartTransaction: this + object should contain an id of a{" "} + withdrawal transaction which is specifically in a{" "} + 'pending_user_transfer_start' status that should be + achieved after the user{" "} + + completes the interactive withdrawal + + . This is an important status on the withdrawal flow + as it signals to the wallet that it's time to send funds to the Anchor + using the provided transaction parameters (aka{" "} + amount_in, amount_in_asset,{" "} + withdraw_anchor_account,{" "} + withdraw_memo, withdraw_memo_type). +
  • +
  • + withdrawCompletedTransaction: this object should + contain an id of a withdrawal{" "} + transaction which is in a 'completed' status usually + achieved after the user receives the funds in the bank account (or + other off-chain rails). This object should also contain a{" "} + stellar_transaction_id parameter which is the id of + the stellar payment transaction used to send funds from wallet to + Anchor. The wallet should be able to fetch the Sep-24 transaction + object through the{" "} + + GET /transaction + {" "} + endpoint using this stellar_transaction_id as the + query parameter. +
  • +
+ SEP-31

SEP-31 configuration objects are used for specifying a variety of data diff --git a/ui/src/components/TestCases/index.tsx b/ui/src/components/TestCases/index.tsx index f4b86df..e5c59b0 100644 --- a/ui/src/components/TestCases/index.tsx +++ b/ui/src/components/TestCases/index.tsx @@ -50,7 +50,7 @@ const SepTests = styled.div` transition: all 0.5s ease-in-out; `; -interface AccordianState { +interface AccordionState { [key: string]: boolean; } @@ -73,7 +73,7 @@ export const TestCases: React.FC<{ runState: RunState; testCases: GroupedTestCases; }> = ({ runState, testCases }) => { - const [accordianState, setAccordianState] = useState({} as AccordianState); + const [accordionState, setAccordionState] = useState({} as AccordionState); return ( <> @@ -83,7 +83,7 @@ export const TestCases: React.FC<{ sep: number; tests: TestCase[]; }) => { - const sepGroupAccordionState = !!accordianState[sepGroup.sep]; + const sepGroupAccordionState = !!accordionState[sepGroup.sep]; return ( @@ -95,8 +95,8 @@ export const TestCases: React.FC<{ - setAccordianState({ - ...accordianState, + setAccordionState({ + ...accordionState, [sepGroup.sep]: !sepGroupAccordionState, }) } @@ -105,7 +105,7 @@ export const TestCases: React.FC<{ {sepGroup.tests.map((testCase: TestCase, i: number) => ( > = { 31: [1, 10, 12, 31], 38: [1, 10, 38], }; -// SEPs that require the config file field to be rendered in UI -const CONFIG_SEPS = [6, 12, 31, 38]; +// SEPs that require the config file field to be rendered on UI +const CONFIG_SEPS = [6, 12, 24, 31, 38]; +// SEPs that require the customer files field to be rendered on UI +const CUSTOMER_FILES_SEPS = [6, 12, 31]; // SEPs that require an asset to use in tests const TRANSFER_SEPS = [6, 24, 31]; @@ -67,6 +69,7 @@ export const TestRunner = () => { const [isModalVisible, setIsModalVisible] = useState(false); const [serverFailure, setServerFailure] = useState(""); const [isConfigNeeded, setIsConfigNeeded] = useState(false); + const [isCustomerFilesNeeded, setIsCustomerFilesNeeded] = useState(false); const [testRunArray, setTestRunArray] = useState([] as GroupedTestCases); const [testRunOrderMap, setTestRunOrderMap] = useState( {} as Record, @@ -87,6 +90,7 @@ export const TestRunner = () => { setRunState(RunState.noTests); setServerFailure(""); setIsConfigNeeded(false); + setIsCustomerFilesNeeded(false); setCustomerImageData([]); setSupportedAssets([]); setSupportedSeps([]); @@ -302,11 +306,23 @@ export const TestRunner = () => { if (sepNumber && CONFIG_SEPS.includes(sepNumber)) { setIsConfigNeeded(true); configValue = formData.sepConfig; + + // Sets default config value for Sep-24 to make sure the "Run Tests" button + // is always enabled allowing Anchors to initially run most of Sep-24 tests + // without actually uploading the config file + if (sepNumber === 24 && !configValue) { + configValue = {}; + } } else { setIsConfigNeeded(false); configValue = undefined; } + // display customer files button only if it is still needed + setIsCustomerFilesNeeded( + Boolean(sepNumber && CUSTOMER_FILES_SEPS.includes(sepNumber)), + ); + const newFormData = { ...formData, sepConfig: configValue, @@ -358,7 +374,7 @@ export const TestRunner = () => { sepConfig: sepConfigObj, }); // remove any images for customers that are not in the SEP-12 config object - const sep12Customers = sepConfigObj["12"]?.customers; + const sep12Customers = sepConfigObj["12"]?.customers || {}; if (Object.keys(sep12Customers).length) { let i = 0; let customerImageDataCopy = [...customerImageData]; @@ -452,67 +468,65 @@ export const TestRunner = () => { )} {isConfigNeeded && ( - <> - - handleFileChange(e.target.files)} - type="file" - accept="application/json" - /> - setIsModalVisible(true)} /> - - setIsModalVisible(false)} - > - - - - - + {customerImageData.some((cid) => Boolean(cid.fileName)) && ( +

+ {customerImageData.reduce( + (prev, cur) => prev + (cur.fileName ? 1 : 0), + 0, + )}{" "} + file + {customerImageData.reduce( + (prev, cur) => prev + (cur.fileName ? 1 : 0), + 0, + ) > 1 + ? "s" + : ""}{" "} + uploaded +

+ )} + setIsImageUploadModalVisible(false)} + > + - Upload Customer Files - - {customerImageData.some((cid) => Boolean(cid.fileName)) && ( -

- {customerImageData.reduce( - (prev, cur) => prev + (cur.fileName ? 1 : 0), - 0, - )}{" "} - file - {customerImageData.reduce( - (prev, cur) => prev + (cur.fileName ? 1 : 0), - 0, - ) > 1 - ? "s" - : ""}{" "} - uploaded -

- )} - setIsImageUploadModalVisible(false)} - > - - - - + setIsImageUploadModalVisible={setIsImageUploadModalVisible} + >
+
+ )} {serverFailure && (