/** * 402md-signer.ts — generated single-file build of @stellar-card/signer. * * Drop into your project and import { createStellarCardSigner }. * Generated by packages/signer/scripts/build-single-file.ts. */ import { Address, Keypair, StrKey, hash, nativeToScVal, xdr } from '@stellar/stellar-sdk'; interface StellarCardConfig { cardContract: string; agentPrivateKey: string; agentRuleId: number; ed25519VerifierAddress: string; network: 'stellar:testnet' | 'stellar:pubnet'; } function decodeAgentKey(hex: string): Uint8Array { if (hex.length !== 64) { throw new Error(`agent key must be 64 hex characters, got ${String(hex.length)}`); } if (!/^[0-9a-fA-F]+$/.test(hex)) { throw new Error('agent key must contain only hex characters'); } const out = new Uint8Array(32); for (let i = 0; i < 32; i++) { out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return out; } function computeAuthDigest( signaturePayload: Uint8Array, contextRuleIds: number[], ): Uint8Array { // Soroban's `Vec::to_xdr(env)` serializes as a full ScVal -- i.e. // ScVal::Vec(Some(ScVec([ScVal::U32(id), ...]))) -- including discriminants. // OZ's __check_auth composes the auth_digest preimage as: // sha256(signature_payload || context_rule_ids.to_xdr(e)) // so we must include the ScVal wrapper here, not just raw u32 bytes. const ruleIdsScVal = xdr.ScVal.scvVec(contextRuleIds.map((id) => xdr.ScVal.scvU32(id))); const ruleIdsXdr = ruleIdsScVal.toXDR(); const composed = new Uint8Array(signaturePayload.length + ruleIdsXdr.length); composed.set(signaturePayload, 0); composed.set(ruleIdsXdr, signaturePayload.length); return new Uint8Array(hash(Buffer.from(composed))); } interface BuildAuthPayloadArgs { verifierAddress: string; agentPubkey: Uint8Array; signature: Uint8Array; contextRuleIds: number[]; } function buildAuthPayloadScVal(args: BuildAuthPayloadArgs): xdr.ScVal { const verifierScVal = nativeToScVal(Address.fromString(args.verifierAddress), { type: 'address', }); const externalSigner = xdr.ScVal.scvVec([ xdr.ScVal.scvSymbol('External'), verifierScVal, xdr.ScVal.scvBytes(Buffer.from(args.agentPubkey)), ]); const signatureBytes = xdr.ScVal.scvBytes(Buffer.from(args.signature)); const signersMap = xdr.ScVal.scvMap([ new xdr.ScMapEntry({ key: externalSigner, val: signatureBytes, }), ]); const contextRuleIdsVec = xdr.ScVal.scvVec(args.contextRuleIds.map((id) => xdr.ScVal.scvU32(id))); // ScMap keys must be sorted in canonical XDR order. For symbols, that is // case-sensitive alphabetical: 'context_rule_ids' < 'signers'. return xdr.ScVal.scvMap([ new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('context_rule_ids'), val: contextRuleIdsVec, }), new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('signers'), val: signersMap, }), ]); } interface PatchTxAuthArgs { /** Base64 XDR of the prepared (post-simulation) transaction. */ txXdr: string; /** Card contract C-address — used to find the right auth entry. */ cardContract: string; /** sha256(networkPassphrase) of the target Stellar network. */ networkId: Buffer; /** Ledger at which the OZ auth signature expires. Must be `<= currentLedger + ceil(maxTimeoutSeconds/5) + 2`. */ sigExpirationLedger: number; /** The agent's Ed25519 keypair (loaded from STELLAR_CARD_AGENT_KEY). */ agentKp: Keypair; /** Agent ed25519 raw public key (32 bytes). */ agentPubkey: Uint8Array; /** Context rule id assigned to the agent (typically 1). */ agentRuleId: number; /** Ed25519 verifier contract C-address. */ verifierAddress: string; } /** * Locates the smart account's auth entry inside the prepared transaction's * `InvokeHostFunctionOp.auth` list and replaces its `signature` field with an * OZ AuthPayload (`{ context_rule_ids, signers: { External(verifier, pubkey) → signature } }`). * * Returns the patched transaction as base64 XDR. The caller must re-simulate * the patched tx in enforce mode to populate the final footprint and resource * fee — see `rebuildWithFreshFootprint`. */ function patchSmartAccountAuth(args: PatchTxAuthArgs): string { const envelope = xdr.TransactionEnvelope.fromXDR(args.txXdr, 'base64'); if (envelope.switch().name !== 'envelopeTypeTx') { throw new Error('expected v1 transaction envelope'); } const innerTx = envelope.v1().tx(); const ops = innerTx.operations(); let patched = 0; for (const op of ops) { const body = op.body(); if (body.switch().name !== 'invokeHostFunction') continue; const ihf = body.value() as xdr.InvokeHostFunctionOp; for (const entry of ihf.auth()) { const creds = entry.credentials(); if (creds.switch().name !== 'sorobanCredentialsAddress') continue; const addrCreds = creds.address(); const addrSc = addrCreds.address(); if (addrSc.switch().name !== 'scAddressTypeContract') continue; const entryContract = StrKey.encodeContract(addrSc.contractId() as unknown as Buffer); if (entryContract !== args.cardContract) continue; // signatureExpirationLedger must be set BEFORE computing the digest // because the digest preimage includes it. addrCreds.signatureExpirationLedger(args.sigExpirationLedger); const nonce = addrCreds.nonce(); const rootInvocation = entry.rootInvocation(); // signature_payload = sha256(HashIdPreimage::SorobanAuthorization{ // network_id, nonce, signature_expiration_ledger, invocation // }) const sigPreimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization( new xdr.HashIdPreimageSorobanAuthorization({ networkId: args.networkId, nonce, signatureExpirationLedger: args.sigExpirationLedger, invocation: rootInvocation, }), ); const signaturePayloadHash = hash(sigPreimage.toXDR()); // OZ auth_digest = sha256(signature_payload || ScVec(rule_ids).toXDR()) const authDigest = computeAuthDigest(signaturePayloadHash, [args.agentRuleId]); // Sign the digest with the agent's Ed25519 key. const signature = args.agentKp.sign(Buffer.from(authDigest)); const authPayload = buildAuthPayloadScVal({ verifierAddress: args.verifierAddress, agentPubkey: args.agentPubkey, signature: new Uint8Array(signature), contextRuleIds: [args.agentRuleId], }); addrCreds.signature(authPayload); patched++; } } if (patched === 0) { throw new Error(`smart account auth entry not found for ${args.cardContract}`); } return envelope.toXDR('base64'); } export type { StellarCardConfig }; export interface PatchTxAuthOptions { /** Base64 XDR of the prepared (post-simulation) transaction. */ txXdr: string; /** sha256(networkPassphrase) of the target network. */ networkId: Buffer; /** Ledger at which the OZ auth signature expires. */ sigExpirationLedger: number; } export interface StellarCardSigner { /** Card contract C-address. */ readonly address: string; /** Agent ed25519 raw public key (32 bytes). */ readonly agentPubkey: Uint8Array; /** * Patches the smart account auth entry inside a prepared Soroban transaction * with an OZ AuthPayload signed by the agent's Ed25519 key. Returns the * patched base64 XDR. The caller must re-simulate to populate the final * footprint before submitting. */ patchTxAuth(opts: PatchTxAuthOptions): string; } export function createStellarCardSigner(config: StellarCardConfig): StellarCardSigner { const seed = decodeAgentKey(config.agentPrivateKey); const keypair = Keypair.fromRawEd25519Seed(Buffer.from(seed)); const agentPubkey = keypair.rawPublicKey(); return { address: config.cardContract, agentPubkey, patchTxAuth(opts: PatchTxAuthOptions): string { return patchSmartAccountAuth({ txXdr: opts.txXdr, cardContract: config.cardContract, networkId: opts.networkId, sigExpirationLedger: opts.sigExpirationLedger, agentKp: keypair, agentPubkey, agentRuleId: config.agentRuleId, verifierAddress: config.ed25519VerifierAddress, }); }, }; }