Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.repo-history-backups/
temp_auto_push.bat
.claude/

.gitignore
branch_structure.json
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"myml.vscode-markdown-plantuml-preview",
"esbenp.prettier-vscode",
"jebbs.plantuml"
]
}
50 changes: 50 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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": ["<node_internals>/**"],
// 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": ["<node_internals>/**"],
"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": ["<node_internals>/**"],
"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.",
}
]
}
23 changes: 23 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
2 changes: 2 additions & 0 deletions .vscode/spellright.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Contentful
Lerna
33 changes: 33 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -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"
}
},
]
}
18 changes: 11 additions & 7 deletions esure-backend/scenarios/issued-asset-payment.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
41 changes: 33 additions & 8 deletions esure-backend/scenarios/missing-trustline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
32 changes: 22 additions & 10 deletions esure-backend/src/scenario-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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);
}

Expand All @@ -202,14 +210,18 @@ function validateAssertionReferences(assertion: ScenarioAssertion, accounts: Set
}

function uniqueIds(items: readonly { id: string }[], label: string, issues: string[]): Set<string> {
const ids = new Set<string>();
const seen = new Set<string>();
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"}`;
});
}
Loading