Problem
app/backend/src/onchain/soroban-onchain.adapter.ts implements the OnchainAdapter interface but cannot actually reach the deployed aid_escrow contract, and it is not wired into the module. Its invokeContract posts a hand-rolled JSON payload to simulateTransaction/sendTransaction that has no XDR transaction envelope, signature, or fee — the shape does not match Soroban RPC at all:
// app/backend/src/onchain/soroban-onchain.adapter.ts
private async invokeContract(method: string, args: unknown[]) {
const sim = await rpcCall(this.http, this.rpcUrl, 'simulateTransaction', {
transaction: JSON.stringify({ contractId: this.contractId, method, args }), // ← not a Soroban envelope
});
const result = await rpcCall(this.http, this.rpcUrl, 'sendTransaction', {
transaction: JSON.stringify({ contractId: this.contractId, method, args,
networkPassphrase: this.networkPassphrase, secret: this.secretKey }), // ← not a signed envelope
});
return ...;
}
It also calls contract methods that do not exist in app/onchain/contracts/aid_escrow/src/lib.rs: initialize (the contract entrypoint is init), claim_package (claim), disburse_package (disburse), and a 6-argument create_package (the contract's create_package takes operator, id, recipient, amount, token, expires_at, metadata). Every mutating method returns transactionHash: '', status: 'success', and amountClaimed: '0'/amountDisbursed: '0' regardless of what the RPC returned.
Consequence: the class fabricates success for every operation. app/backend/src/onchain/onchain.module.ts only registers MockOnchainAdapter and SorobanAdapter (the real, @stellar/stellar-sdk-based implementation in soroban.adapter.ts), so SorobanOnchainAdapter is dead code — but it is exported, named confusingly close to SorobanAdapter, and reads as a working adapter. Any contributor (or future ONCHAIN_ADAPTER config value) that selects it will observe success: true for create/claim/disburse while nothing is written on-chain, silently corrupting the audit trail this platform is built around.
Root cause
SorobanOnchainAdapter was a first-draft stub built on @nestjs/axios with a fabricated JSON RPC shape, then superseded by SorobanAdapter but never deleted or marked deprecated; its method names were never reconciled with the contract ABI.
Why this is architecturally hard
- Two real options and one trap. The straightforward fix is deletion, but the existence of a second
*soroban*.adapter.ts invites re-wiring. The decision is whether to remove SorobanOnchainAdapter entirely (and its export) or fold any useful logic into SorobanAdapter; leaving both is the failure mode.
- The adapter interface masks the gap.
OnchainAdapter (onchain.adapter.ts) returns status: 'success' | 'failed' and string transactionHash from every method, so a stub can satisfy the type system while doing nothing. Fixing the class alone does not stop future stubs; the interface or a validateConfig/ensureConfigured gate (as SorobanAdapter has) is the real guardrail.
- Tests assert the wrong thing.
onchain.module.spec.ts asserts an adapter instance is created for ONCHAIN_ADAPTER=soroban, not that calls reach a contract. A regression test must simulate the RPC boundary to catch a fake payload.
Proposed design
Remove soroban-onchain.adapter.ts and its export { ONCHAIN_ADAPTER_TOKEN } duplicate, keeping the single canonical SorobanAdapter (or, if retained, make it share SorobanAdapter's ensureConfigured()/validateConfig() and @stellar/stellar-sdk submission path). Add a unit test that instantiates each registered adapter and asserts a submit call produces a real TransactionBuilder-built envelope (not a JSON blob).
Downstream impact
No ABI change — this is backend-internal. If the class is removed, confirm nothing imports it (the only reference is its own file); onchain.module.ts already excludes it from providers.
Acceptance criteria
Service
Tests
Out of scope
Recipient-key signing for claims (the admin-keypair signing gap) and ledger reconciliation are separate issues.
Getting started
Files: app/backend/src/onchain/soroban-onchain.adapter.ts, app/backend/src/onchain/soroban.adapter.ts, app/backend/src/onchain/onchain.module.ts, app/backend/src/onchain/onchain.module.spec.ts.
cd app/backend
npm test
npm run lint:check
Good first files to read: soroban.adapter.ts (the correct submitContractOp flow) and onchain.module.ts (what is actually registered).
Problem
app/backend/src/onchain/soroban-onchain.adapter.tsimplements theOnchainAdapterinterface but cannot actually reach the deployedaid_escrowcontract, and it is not wired into the module. ItsinvokeContractposts a hand-rolled JSON payload tosimulateTransaction/sendTransactionthat has no XDR transaction envelope, signature, or fee — the shape does not match Soroban RPC at all:It also calls contract methods that do not exist in
app/onchain/contracts/aid_escrow/src/lib.rs:initialize(the contract entrypoint isinit),claim_package(claim),disburse_package(disburse), and a 6-argumentcreate_package(the contract'screate_packagetakesoperator, id, recipient, amount, token, expires_at, metadata). Every mutating method returnstransactionHash: '',status: 'success', andamountClaimed: '0'/amountDisbursed: '0'regardless of what the RPC returned.Consequence: the class fabricates success for every operation.
app/backend/src/onchain/onchain.module.tsonly registersMockOnchainAdapterandSorobanAdapter(the real,@stellar/stellar-sdk-based implementation insoroban.adapter.ts), soSorobanOnchainAdapteris dead code — but it is exported, named confusingly close toSorobanAdapter, and reads as a working adapter. Any contributor (or futureONCHAIN_ADAPTERconfig value) that selects it will observesuccess: truefor create/claim/disburse while nothing is written on-chain, silently corrupting the audit trail this platform is built around.Root cause
SorobanOnchainAdapterwas a first-draft stub built on@nestjs/axioswith a fabricated JSON RPC shape, then superseded bySorobanAdapterbut never deleted or marked deprecated; its method names were never reconciled with the contract ABI.Why this is architecturally hard
*soroban*.adapter.tsinvites re-wiring. The decision is whether to removeSorobanOnchainAdapterentirely (and its export) or fold any useful logic intoSorobanAdapter; leaving both is the failure mode.OnchainAdapter(onchain.adapter.ts) returnsstatus: 'success' | 'failed'and stringtransactionHashfrom every method, so a stub can satisfy the type system while doing nothing. Fixing the class alone does not stop future stubs; the interface or avalidateConfig/ensureConfiguredgate (asSorobanAdapterhas) is the real guardrail.onchain.module.spec.tsasserts an adapter instance is created forONCHAIN_ADAPTER=soroban, not that calls reach a contract. A regression test must simulate the RPC boundary to catch a fake payload.Proposed design
Remove
soroban-onchain.adapter.tsand itsexport { ONCHAIN_ADAPTER_TOKEN }duplicate, keeping the single canonicalSorobanAdapter(or, if retained, make it shareSorobanAdapter'sensureConfigured()/validateConfig()and@stellar/stellar-sdksubmission path). Add a unit test that instantiates each registered adapter and asserts a submit call produces a realTransactionBuilder-built envelope (not a JSON blob).Downstream impact
No ABI change — this is backend-internal. If the class is removed, confirm nothing imports it (the only reference is its own file);
onchain.module.tsalready excludes it from providers.Acceptance criteria
Service
soroban-onchain.adapter.tsis deleted or reimplemented to sign and submit real Soroban envelopes, and exactly one canonical Soroban adapter remains for thesorobanconfig value.status: 'success'with an emptytransactionHashafter a failed or never-sent transaction.Tests
simulateTransactionpayload is not a valid Soroban envelope (no{contractId, method, args}JSON blob), and passes forSorobanAdapter.Out of scope
Recipient-key signing for claims (the admin-keypair signing gap) and ledger reconciliation are separate issues.
Getting started
Files:
app/backend/src/onchain/soroban-onchain.adapter.ts,app/backend/src/onchain/soroban.adapter.ts,app/backend/src/onchain/onchain.module.ts,app/backend/src/onchain/onchain.module.spec.ts.Good first files to read:
soroban.adapter.ts(the correctsubmitContractOpflow) andonchain.module.ts(what is actually registered).