forked from Nodal-stellar/Nodal-AI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMultiSigPaymentTool.ts
More file actions
115 lines (99 loc) · 3.81 KB
/
Copy pathMultiSigPaymentTool.ts
File metadata and controls
115 lines (99 loc) · 3.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* backend/tools/MultiSigPaymentTool.ts
* Build M-of-N multi-signature payment transactions for high-value PayFi operations.
*/
import {
Keypair,
TransactionBuilder,
Operation,
Asset,
BASE_FEE,
Memo,
StrKey,
xdr,
} from "@stellar/stellar-sdk";
import { z } from "zod";
import { config } from "../config";
import { loadAccount, submitTransaction, resolveNetworkPassphrase } from "../rpc_client";
import { SubmitResultSchema } from "./StellarPaymentTool";
export const MultiSigInputSchema = z.object({
destination: z.string().length(56, "Invalid Stellar public key"),
amount: z
.string()
.regex(/^(?!0(\.0+)?$)\d+(\.\d{1,7})?$/, "Amount must be a valid Stellar decimal")
.refine((v) => parseFloat(v) > 0, "Amount must be greater than zero"),
assetCode: z.string().default("XLM"),
assetIssuer: z.string().optional(),
memo: z
.string()
.refine((v) => Buffer.byteLength(v, "utf8") <= 28, "Memo must be at most 28 bytes")
.optional(),
additionalSigners: z.array(
z
.string()
.length(56, "Invalid signer public key")
.refine((value) => StrKey.isValidEd25519PublicKey(value), "Invalid signer public key")
),
minSignatures: z.number().int().min(1),
signatures: z.array(z.string()).optional(),
});
export type MultiSigInput = z.infer<typeof MultiSigInputSchema>;
export interface MultiSigResult {
/** Unsigned transaction XDR — returned when signatures are not yet provided */
unsignedXDR?: string;
/** Settled transaction hash — returned when enough signatures were provided */
txHash?: string;
ledger?: number;
}
export class MultiSigPaymentTool {
private keypair: Keypair;
private networkPassphrase: string;
constructor(secretKey: string = config.agentKeypair().secret()) {
this.keypair = Keypair.fromSecret(secretKey);
this.networkPassphrase = resolveNetworkPassphrase(config.STELLAR_NETWORK);
}
async execute(rawInput: unknown): Promise<MultiSigResult> {
const input = MultiSigInputSchema.parse(rawInput);
if (input.minSignatures > input.additionalSigners.length + 1) {
throw new Error(
`minSignatures (${input.minSignatures}) exceeds total available signers (${input.additionalSigners.length + 1})`
);
}
if (input.assetCode !== "XLM" && !input.assetIssuer) {
throw new Error(`Asset issuer is required for non-native asset ${input.assetCode}`);
}
const asset =
input.assetCode === "XLM"
? Asset.native()
: new Asset(input.assetCode, input.assetIssuer!);
const account = await loadAccount(this.keypair.publicKey());
const builder = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: this.networkPassphrase,
}).addOperation(
Operation.payment({ destination: input.destination, asset, amount: input.amount })
);
if (input.memo) {
builder.addMemo(Memo.text(input.memo));
}
const tx = builder.setTimeout(30).build();
// If pre-collected signatures are provided and meet threshold, sign and submit
if (input.signatures && input.signatures.length >= input.minSignatures) {
// Agent signs first
tx.sign(this.keypair);
// Apply additional signatures from provided decorated signatures (XDR-encoded)
for (const sigXdr of input.signatures) {
try {
const decoratedSig = xdr.DecoratedSignature.fromXDR(sigXdr, "base64");
tx.addDecoratedSignature(decoratedSig);
} catch (err) {
throw new Error(`Invalid signature XDR: ${err instanceof Error ? err.message : String(err)}`);
}
}
const result = SubmitResultSchema.parse(await submitTransaction(tx));
return { txHash: result.hash, ledger: result.ledger };
}
// No sufficient signatures yet — return unsigned XDR for external collection
return { unsignedXDR: tx.toXDR() };
}
}