From 7d11d8532c752be041694df2b6be8db495dd03da Mon Sep 17 00:00:00 2001 From: Warm Idris Date: Fri, 6 Mar 2026 16:14:43 +0000 Subject: [PATCH] fix: use correct SIP-018 message format in acceptIncomingTransfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs caused the agent to produce signatures incompatible with on-chain verification: 1. Message was nested (pipe-key as sub-object) with plain string values. The Clarity contract uses a flat merged tuple with typed uint/principal fields — matching what sip018_sign expects. 2. balance-1/balance-2 always used myBalance/theirBalance regardless of whether the local agent is principal-1 or principal-2. The contract's map-balances logic assigns balance-1 to principal-1's balance canonically, so we must resolve ordering from pipeKey before building the message. Co-Authored-By: Claude Sonnet 4.6 --- packages/stackflow-agent/src/agent-service.js | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/stackflow-agent/src/agent-service.js b/packages/stackflow-agent/src/agent-service.js index 707b48b..5c5461f 100644 --- a/packages/stackflow-agent/src/agent-service.js +++ b/packages/stackflow-agent/src/agent-service.js @@ -407,18 +407,40 @@ export class StackflowAgentService { } const state = validation.state; + + // Build the flat Clarity-typed message matching the on-chain SIP-018 domain. + // balance-1 always corresponds to principal-1 in the pipe key (canonical ordering), + // regardless of which side the local agent is on. + const pipeKey = state.pipeKey; + const localIsPrincipal1 = pipeKey["principal-1"] === state.forPrincipal; + const balance1 = localIsPrincipal1 ? state.myBalance : state.theirBalance; + const balance2 = localIsPrincipal1 ? state.theirBalance : state.myBalance; + + const message = { + "principal-1": { type: "principal", value: pipeKey["principal-1"] }, + "principal-2": { type: "principal", value: pipeKey["principal-2"] }, + token: + pipeKey.token == null + ? { type: "none" } + : { type: "some", value: { type: "principal", value: String(pipeKey.token) } }, + "balance-1": { type: "uint", value: Number(balance1) }, + "balance-2": { type: "uint", value: Number(balance2) }, + nonce: { type: "uint", value: Number(state.nonce) }, + action: { type: "uint", value: Number(state.action) }, + actor: { type: "principal", value: state.actor }, + "hashed-secret": + state.secret == null + ? { type: "none" } + : { type: "some", value: { type: "buff", value: state.secret } }, + "valid-after": + state.validAfter == null + ? { type: "none" } + : { type: "some", value: { type: "uint", value: Number(state.validAfter) } }, + }; + const mySignature = await this.signTransferMessage({ contractId: state.contractId, - message: { - "pipe-key": state.pipeKey, - "balance-1": state.myBalance, - "balance-2": state.theirBalance, - nonce: state.nonce, - action: state.action, - actor: state.actor, - "hashed-secret": state.secret, - "valid-after": state.validAfter, - }, + message, walletPassword, });