@ophirai/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +139 -0
  2. package/dist/__tests__/buyer.test.d.ts +1 -0
  3. package/dist/__tests__/buyer.test.js +664 -0
  4. package/dist/__tests__/discovery.test.d.ts +1 -0
  5. package/dist/__tests__/discovery.test.js +188 -0
  6. package/dist/__tests__/escrow.test.d.ts +1 -0
  7. package/dist/__tests__/escrow.test.js +385 -0
  8. package/dist/__tests__/identity.test.d.ts +1 -0
  9. package/dist/__tests__/identity.test.js +222 -0
  10. package/dist/__tests__/integration.test.d.ts +1 -0
  11. package/dist/__tests__/integration.test.js +681 -0
  12. package/dist/__tests__/lockstep.test.d.ts +1 -0
  13. package/dist/__tests__/lockstep.test.js +320 -0
  14. package/dist/__tests__/messages.test.d.ts +1 -0
  15. package/dist/__tests__/messages.test.js +976 -0
  16. package/dist/__tests__/negotiation.test.d.ts +1 -0
  17. package/dist/__tests__/negotiation.test.js +667 -0
  18. package/dist/__tests__/seller.test.d.ts +1 -0
  19. package/dist/__tests__/seller.test.js +767 -0
  20. package/dist/__tests__/server.test.d.ts +1 -0
  21. package/dist/__tests__/server.test.js +239 -0
  22. package/dist/__tests__/signing.test.d.ts +1 -0
  23. package/dist/__tests__/signing.test.js +713 -0
  24. package/dist/__tests__/sla.test.d.ts +1 -0
  25. package/dist/__tests__/sla.test.js +342 -0
  26. package/dist/__tests__/transport.test.d.ts +1 -0
  27. package/dist/__tests__/transport.test.js +197 -0
  28. package/dist/__tests__/x402.test.d.ts +1 -0
  29. package/dist/__tests__/x402.test.js +141 -0
  30. package/dist/buyer.d.ts +190 -0
  31. package/dist/buyer.js +555 -0
  32. package/dist/discovery.d.ts +47 -0
  33. package/dist/discovery.js +51 -0
  34. package/dist/escrow.d.ts +177 -0
  35. package/dist/escrow.js +434 -0
  36. package/dist/identity.d.ts +60 -0
  37. package/dist/identity.js +108 -0
  38. package/dist/index.d.ts +122 -0
  39. package/dist/index.js +43 -0
  40. package/dist/lockstep.d.ts +94 -0
  41. package/dist/lockstep.js +127 -0
  42. package/dist/messages.d.ts +172 -0
  43. package/dist/messages.js +262 -0
  44. package/dist/negotiation.d.ts +113 -0
  45. package/dist/negotiation.js +214 -0
  46. package/dist/seller.d.ts +127 -0
  47. package/dist/seller.js +395 -0
  48. package/dist/server.d.ts +52 -0
  49. package/dist/server.js +149 -0
  50. package/dist/signing.d.ts +98 -0
  51. package/dist/signing.js +165 -0
  52. package/dist/sla.d.ts +95 -0
  53. package/dist/sla.js +187 -0
  54. package/dist/transport.d.ts +41 -0
  55. package/dist/transport.js +127 -0
  56. package/dist/types.d.ts +86 -0
  57. package/dist/types.js +1 -0
  58. package/dist/x402.d.ts +25 -0
  59. package/dist/x402.js +54 -0
  60. package/package.json +40 -0
@@ -0,0 +1,177 @@
1
+ import type { Agreement } from './types.js';
2
+ /** Escrow account status as stored on-chain. */
3
+ export type EscrowStatus = 'Active' | 'Released' | 'Disputed' | 'Cancelled';
4
+ /** On-chain escrow account data. */
5
+ export interface EscrowAccountData {
6
+ buyer: string;
7
+ seller: string;
8
+ mint: string;
9
+ agreementHash: string;
10
+ depositAmount: bigint;
11
+ penaltyRateBps: number;
12
+ createdAt: number;
13
+ timeoutSlot: bigint;
14
+ status: EscrowStatus;
15
+ bump: number;
16
+ }
17
+ /**
18
+ * Manages Solana escrow PDAs for agreement payments and dispute resolution.
19
+ *
20
+ * Derives deterministic PDA addresses from buyer pubkey and agreement hash,
21
+ * ensuring collision-resistant escrow accounts tied to specific agreements.
22
+ * Builds and submits Solana transactions for all escrow lifecycle operations.
23
+ */
24
+ export declare class EscrowManager {
25
+ private rpcUrl;
26
+ private programId;
27
+ constructor(config?: {
28
+ rpcUrl?: string;
29
+ programId?: string;
30
+ });
31
+ /**
32
+ * Derive the deterministic escrow PDA address from buyer pubkey and agreement hash.
33
+ * Seeds: ["escrow", buyer_pubkey, agreement_hash]
34
+ *
35
+ * @param buyerPublicKey - 32-byte Ed25519 public key of the buyer
36
+ * @param agreementHash - SHA-256 hash of the canonicalized agreement terms (32 bytes)
37
+ * @returns The base58-encoded PDA address and its bump seed
38
+ * @throws {OphirError} INVALID_MESSAGE if buyerPublicKey is not 32 bytes
39
+ */
40
+ deriveEscrowAddress(buyerPublicKey: Uint8Array, agreementHash: Uint8Array): {
41
+ address: string;
42
+ bump: number;
43
+ };
44
+ /**
45
+ * Derive the vault token account PDA from an escrow address.
46
+ * Seeds: ["vault", escrow_pubkey]
47
+ *
48
+ * @param escrowPublicKey - 32-byte public key of the escrow PDA
49
+ * @returns The base58-encoded vault PDA address and its bump seed
50
+ */
51
+ deriveVaultAddress(escrowPublicKey: Uint8Array): {
52
+ address: string;
53
+ bump: number;
54
+ };
55
+ /**
56
+ * Create an escrow account and deposit tokens into the PDA-controlled vault.
57
+ *
58
+ * Builds a `make_escrow` instruction with the buyer's token account as the
59
+ * funding source, initializes the escrow PDA and vault, and submits the
60
+ * transaction to the Solana network.
61
+ *
62
+ * @param params.agreement - The signed agreement containing the hash to escrow
63
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (signs the transaction)
64
+ * @param params.sellerPublicKey - Seller's 32-byte public key
65
+ * @param params.depositAmount - Amount in smallest token units (e.g., USDC has 6 decimals)
66
+ * @param params.mintAddress - SPL token mint address (e.g., USDC mint)
67
+ * @param params.buyerTokenAccount - Buyer's associated token account address
68
+ * @param params.timeoutSlots - Slots before escrow can be cancelled (default: ~24h)
69
+ * @param params.penaltyRateBps - Max penalty in basis points (default: 500 = 5%)
70
+ * @returns The escrow PDA address, vault address, and transaction signature
71
+ * @throws {OphirError} ESCROW_CREATION_FAILED if the transaction fails
72
+ * @throws {OphirError} INVALID_MESSAGE if inputs are invalid
73
+ */
74
+ createEscrow(params: {
75
+ agreement: Agreement;
76
+ buyerKeypair: {
77
+ publicKey: Uint8Array;
78
+ secretKey: Uint8Array;
79
+ };
80
+ sellerPublicKey: Uint8Array;
81
+ depositAmount: bigint;
82
+ mintAddress: string;
83
+ buyerTokenAccount: string;
84
+ timeoutSlots?: number;
85
+ penaltyRateBps?: number;
86
+ }): Promise<{
87
+ escrowAddress: string;
88
+ vaultAddress: string;
89
+ txSignature: string;
90
+ }>;
91
+ /**
92
+ * Release escrowed funds to the seller after successful job completion.
93
+ *
94
+ * Only the seller can call this instruction. Transfers the entire vault
95
+ * balance to the seller's token account and marks the escrow as Released.
96
+ *
97
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
98
+ * @param params.sellerKeypair - Seller's Ed25519 keypair (must match escrow.seller)
99
+ * @param params.sellerTokenAccount - Seller's token account to receive funds
100
+ * @param params.verificationHash - Optional 32-byte proof of service delivery
101
+ * @returns The transaction signature
102
+ * @throws {OphirError} ESCROW_ALREADY_RELEASED if the escrow is not Active
103
+ */
104
+ releaseEscrow(params: {
105
+ escrowAddress: string;
106
+ sellerKeypair: {
107
+ publicKey: Uint8Array;
108
+ secretKey: Uint8Array;
109
+ };
110
+ sellerTokenAccount: string;
111
+ verificationHash?: Uint8Array;
112
+ }): Promise<{
113
+ txSignature: string;
114
+ }>;
115
+ /**
116
+ * File an on-chain dispute, splitting funds between buyer (penalty) and seller (remainder).
117
+ *
118
+ * Only the buyer can initiate a dispute. The penalty amount must not exceed
119
+ * `deposit_amount * penalty_rate_bps / 10000`. The penalty is returned to
120
+ * the buyer and the remainder goes to the seller.
121
+ *
122
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
123
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (must match escrow.buyer)
124
+ * @param params.buyerTokenAccount - Buyer's token account for penalty refund
125
+ * @param params.sellerTokenAccount - Seller's token account for remainder
126
+ * @param params.evidenceHash - 32-byte SHA-256 hash of the violation evidence
127
+ * @param params.penaltyAmount - Penalty in smallest token units
128
+ * @returns The transaction signature
129
+ * @throws {OphirError} ESCROW_VERIFICATION_FAILED if penalty exceeds max allowed
130
+ */
131
+ disputeEscrow(params: {
132
+ escrowAddress: string;
133
+ buyerKeypair: {
134
+ publicKey: Uint8Array;
135
+ secretKey: Uint8Array;
136
+ };
137
+ buyerTokenAccount: string;
138
+ sellerTokenAccount: string;
139
+ evidenceHash: Uint8Array;
140
+ penaltyAmount: bigint;
141
+ }): Promise<{
142
+ txSignature: string;
143
+ }>;
144
+ /**
145
+ * Cancel an escrow after the timeout slot has passed, returning all funds to the buyer.
146
+ *
147
+ * Only the buyer can cancel, and only after the escrow's timeout_slot has been reached.
148
+ * Transfers the entire vault balance back to the buyer's token account.
149
+ *
150
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
151
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (must match escrow.buyer)
152
+ * @param params.buyerTokenAccount - Buyer's token account for refund
153
+ * @returns The transaction signature
154
+ * @throws {OphirError} ESCROW_TIMEOUT_NOT_REACHED if timeout has not elapsed
155
+ */
156
+ cancelEscrow(params: {
157
+ escrowAddress: string;
158
+ buyerKeypair: {
159
+ publicKey: Uint8Array;
160
+ secretKey: Uint8Array;
161
+ };
162
+ buyerTokenAccount: string;
163
+ }): Promise<{
164
+ txSignature: string;
165
+ }>;
166
+ /**
167
+ * Fetch and deserialize escrow account data from Solana.
168
+ *
169
+ * Connects to the configured Solana RPC endpoint, fetches the raw account
170
+ * data at the given address, and deserializes it into a typed EscrowAccountData.
171
+ *
172
+ * @param escrowAddress - Base58-encoded escrow PDA address
173
+ * @returns The deserialized escrow account data
174
+ * @throws {OphirError} SOLANA_RPC_ERROR if the account doesn't exist or deserialization fails
175
+ */
176
+ getEscrowStatus(escrowAddress: string): Promise<EscrowAccountData>;
177
+ }
package/dist/escrow.js ADDED
@@ -0,0 +1,434 @@
1
+ import { Connection, Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction, sendAndConfirmTransaction, } from '@solana/web3.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { OphirError, OphirErrorCode, DEFAULT_CONFIG, ESCROW_PROGRAM_ID } from '@ophirai/protocol';
4
+ const DEFAULT_RPC_URL = DEFAULT_CONFIG.solana_rpc;
5
+ const DEFAULT_PROGRAM_ID = ESCROW_PROGRAM_ID;
6
+ const DEFAULT_TIMEOUT_SLOTS = 216_000; // ~24h at 400ms slots
7
+ const DEFAULT_PENALTY_RATE_BPS = 500; // 5%
8
+ const SPL_TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
9
+ /** Numeric status discriminant matching the Anchor program's EscrowStatus enum. */
10
+ const ESCROW_STATUS_MAP = {
11
+ 0: 'Active',
12
+ 1: 'Released',
13
+ 2: 'Disputed',
14
+ 3: 'Cancelled',
15
+ };
16
+ /**
17
+ * Compute the Anchor instruction discriminator for a given instruction name.
18
+ * Anchor uses sha256("global:<name>")[0..8] as the 8-byte discriminator.
19
+ */
20
+ function anchorDiscriminator(name) {
21
+ const hash = createHash('sha256').update(`global:${name}`).digest();
22
+ return Buffer.from(hash.subarray(0, 8));
23
+ }
24
+ /** Anchor discriminators for each escrow instruction. */
25
+ const DISCRIMINATORS = {
26
+ makeEscrow: anchorDiscriminator('make_escrow'),
27
+ releaseEscrow: anchorDiscriminator('release_escrow'),
28
+ disputeEscrow: anchorDiscriminator('dispute_escrow'),
29
+ cancelEscrow: anchorDiscriminator('cancel_escrow'),
30
+ };
31
+ /** Anchor account discriminator for EscrowAccount (sha256("account:EscrowAccount")[0..8]). */
32
+ const ACCOUNT_DISCRIMINATOR = createHash('sha256')
33
+ .update('account:EscrowAccount')
34
+ .digest()
35
+ .subarray(0, 8);
36
+ /** Size of the EscrowAccount: 8 (discriminator) + 32*3 + 32 + 8 + 2 + 8 + 8 + 1 + 1 = 164 bytes. */
37
+ const ESCROW_ACCOUNT_SIZE = 8 + 32 + 32 + 32 + 32 + 8 + 2 + 8 + 8 + 1 + 1;
38
+ /**
39
+ * Serialize make_escrow instruction data in Borsh format.
40
+ * Layout: discriminator(8) + agreement_hash(32) + deposit_amount(u64) + timeout_slots(u64) + penalty_rate_bps(u16)
41
+ */
42
+ function serializeMakeEscrow(agreementHash, depositAmount, timeoutSlots, penaltyRateBps) {
43
+ const buf = Buffer.alloc(8 + 32 + 8 + 8 + 2);
44
+ let offset = 0;
45
+ DISCRIMINATORS.makeEscrow.copy(buf, offset);
46
+ offset += 8;
47
+ Buffer.from(agreementHash).copy(buf, offset);
48
+ offset += 32;
49
+ buf.writeBigUInt64LE(depositAmount, offset);
50
+ offset += 8;
51
+ buf.writeBigUInt64LE(timeoutSlots, offset);
52
+ offset += 8;
53
+ buf.writeUInt16LE(penaltyRateBps, offset);
54
+ return buf;
55
+ }
56
+ /**
57
+ * Serialize release_escrow instruction data in Borsh format.
58
+ * Layout: discriminator(8) + option_flag(1) + [verification_hash(32) if Some]
59
+ */
60
+ function serializeReleaseEscrow(verificationHash) {
61
+ if (verificationHash) {
62
+ const buf = Buffer.alloc(8 + 1 + 32);
63
+ DISCRIMINATORS.releaseEscrow.copy(buf, 0);
64
+ buf.writeUInt8(1, 8); // Some
65
+ Buffer.from(verificationHash).copy(buf, 9);
66
+ return buf;
67
+ }
68
+ const buf = Buffer.alloc(8 + 1);
69
+ DISCRIMINATORS.releaseEscrow.copy(buf, 0);
70
+ buf.writeUInt8(0, 8); // None
71
+ return buf;
72
+ }
73
+ /**
74
+ * Serialize dispute_escrow instruction data in Borsh format.
75
+ * Layout: discriminator(8) + violation_evidence_hash(32) + penalty_amount(u64)
76
+ */
77
+ function serializeDisputeEscrow(evidenceHash, penaltyAmount) {
78
+ const buf = Buffer.alloc(8 + 32 + 8);
79
+ DISCRIMINATORS.disputeEscrow.copy(buf, 0);
80
+ Buffer.from(evidenceHash).copy(buf, 8);
81
+ buf.writeBigUInt64LE(penaltyAmount, 40);
82
+ return buf;
83
+ }
84
+ /**
85
+ * Serialize cancel_escrow instruction data in Borsh format.
86
+ * Layout: discriminator(8) only — no additional args.
87
+ */
88
+ function serializeCancelEscrow() {
89
+ const buf = Buffer.alloc(8);
90
+ DISCRIMINATORS.cancelEscrow.copy(buf, 0);
91
+ return buf;
92
+ }
93
+ /**
94
+ * Deserialize on-chain EscrowAccount data from raw account bytes.
95
+ *
96
+ * Layout (after 8-byte Anchor discriminator):
97
+ * buyer: Pubkey(32) + seller: Pubkey(32) + mint: Pubkey(32) +
98
+ * agreement_hash: [u8;32](32) + deposit_amount: u64(8) +
99
+ * penalty_rate_bps: u16(2) + created_at: i64(8) + timeout_slot: u64(8) +
100
+ * status: u8(1) + bump: u8(1)
101
+ */
102
+ function deserializeEscrowAccount(data) {
103
+ if (data.length < ESCROW_ACCOUNT_SIZE) {
104
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, `Invalid escrow account data: expected at least ${ESCROW_ACCOUNT_SIZE} bytes, got ${data.length}`);
105
+ }
106
+ // Verify Anchor discriminator
107
+ const disc = data.subarray(0, 8);
108
+ if (!disc.equals(Buffer.from(ACCOUNT_DISCRIMINATOR))) {
109
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, 'Invalid escrow account discriminator — account does not belong to the Ophir escrow program');
110
+ }
111
+ let offset = 8;
112
+ const buyer = new PublicKey(data.subarray(offset, offset + 32)).toBase58();
113
+ offset += 32;
114
+ const seller = new PublicKey(data.subarray(offset, offset + 32)).toBase58();
115
+ offset += 32;
116
+ const mint = new PublicKey(data.subarray(offset, offset + 32)).toBase58();
117
+ offset += 32;
118
+ const agreementHash = Buffer.from(data.subarray(offset, offset + 32)).toString('hex');
119
+ offset += 32;
120
+ const depositAmount = data.readBigUInt64LE(offset);
121
+ offset += 8;
122
+ const penaltyRateBps = data.readUInt16LE(offset);
123
+ offset += 2;
124
+ const createdAt = Number(data.readBigInt64LE(offset));
125
+ offset += 8;
126
+ const timeoutSlot = data.readBigUInt64LE(offset);
127
+ offset += 8;
128
+ const statusByte = data.readUInt8(offset);
129
+ offset += 1;
130
+ const bump = data.readUInt8(offset);
131
+ const status = ESCROW_STATUS_MAP[statusByte];
132
+ if (!status) {
133
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, `Unknown escrow status byte: ${statusByte}`);
134
+ }
135
+ return {
136
+ buyer,
137
+ seller,
138
+ mint,
139
+ agreementHash,
140
+ depositAmount,
141
+ penaltyRateBps,
142
+ createdAt,
143
+ timeoutSlot,
144
+ status,
145
+ bump,
146
+ };
147
+ }
148
+ /**
149
+ * Manages Solana escrow PDAs for agreement payments and dispute resolution.
150
+ *
151
+ * Derives deterministic PDA addresses from buyer pubkey and agreement hash,
152
+ * ensuring collision-resistant escrow accounts tied to specific agreements.
153
+ * Builds and submits Solana transactions for all escrow lifecycle operations.
154
+ */
155
+ export class EscrowManager {
156
+ rpcUrl;
157
+ programId;
158
+ constructor(config) {
159
+ this.rpcUrl = config?.rpcUrl ?? DEFAULT_RPC_URL;
160
+ this.programId = new PublicKey(config?.programId ?? DEFAULT_PROGRAM_ID);
161
+ }
162
+ /**
163
+ * Derive the deterministic escrow PDA address from buyer pubkey and agreement hash.
164
+ * Seeds: ["escrow", buyer_pubkey, agreement_hash]
165
+ *
166
+ * @param buyerPublicKey - 32-byte Ed25519 public key of the buyer
167
+ * @param agreementHash - SHA-256 hash of the canonicalized agreement terms (32 bytes)
168
+ * @returns The base58-encoded PDA address and its bump seed
169
+ * @throws {OphirError} INVALID_MESSAGE if buyerPublicKey is not 32 bytes
170
+ */
171
+ deriveEscrowAddress(buyerPublicKey, agreementHash) {
172
+ if (buyerPublicKey.length !== 32) {
173
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid buyer public key length: expected 32, got ${buyerPublicKey.length}`);
174
+ }
175
+ if (agreementHash.length !== 32) {
176
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid agreement hash length: expected 32, got ${agreementHash.length}`);
177
+ }
178
+ const [pda, bump] = PublicKey.findProgramAddressSync([Buffer.from('escrow'), buyerPublicKey, agreementHash], this.programId);
179
+ return { address: pda.toBase58(), bump };
180
+ }
181
+ /**
182
+ * Derive the vault token account PDA from an escrow address.
183
+ * Seeds: ["vault", escrow_pubkey]
184
+ *
185
+ * @param escrowPublicKey - 32-byte public key of the escrow PDA
186
+ * @returns The base58-encoded vault PDA address and its bump seed
187
+ */
188
+ deriveVaultAddress(escrowPublicKey) {
189
+ const [pda, bump] = PublicKey.findProgramAddressSync([Buffer.from('vault'), escrowPublicKey], this.programId);
190
+ return { address: pda.toBase58(), bump };
191
+ }
192
+ /**
193
+ * Create an escrow account and deposit tokens into the PDA-controlled vault.
194
+ *
195
+ * Builds a `make_escrow` instruction with the buyer's token account as the
196
+ * funding source, initializes the escrow PDA and vault, and submits the
197
+ * transaction to the Solana network.
198
+ *
199
+ * @param params.agreement - The signed agreement containing the hash to escrow
200
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (signs the transaction)
201
+ * @param params.sellerPublicKey - Seller's 32-byte public key
202
+ * @param params.depositAmount - Amount in smallest token units (e.g., USDC has 6 decimals)
203
+ * @param params.mintAddress - SPL token mint address (e.g., USDC mint)
204
+ * @param params.buyerTokenAccount - Buyer's associated token account address
205
+ * @param params.timeoutSlots - Slots before escrow can be cancelled (default: ~24h)
206
+ * @param params.penaltyRateBps - Max penalty in basis points (default: 500 = 5%)
207
+ * @returns The escrow PDA address, vault address, and transaction signature
208
+ * @throws {OphirError} ESCROW_CREATION_FAILED if the transaction fails
209
+ * @throws {OphirError} INVALID_MESSAGE if inputs are invalid
210
+ */
211
+ async createEscrow(params) {
212
+ if (params.depositAmount <= 0n) {
213
+ throw new OphirError(OphirErrorCode.ESCROW_CREATION_FAILED, 'Deposit amount must be greater than zero');
214
+ }
215
+ if (params.sellerPublicKey.length !== 32) {
216
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid seller public key length: expected 32, got ${params.sellerPublicKey.length}`);
217
+ }
218
+ const timeoutSlots = params.timeoutSlots ?? DEFAULT_TIMEOUT_SLOTS;
219
+ const penaltyRateBps = params.penaltyRateBps ?? DEFAULT_PENALTY_RATE_BPS;
220
+ if (penaltyRateBps > 10000) {
221
+ throw new OphirError(OphirErrorCode.ESCROW_CREATION_FAILED, `Penalty rate ${penaltyRateBps} exceeds maximum 10000 basis points`);
222
+ }
223
+ const hashBytes = Buffer.from(params.agreement.agreement_hash, 'hex');
224
+ if (hashBytes.length !== 32) {
225
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid agreement hash: expected 64 hex chars (32 bytes), got ${params.agreement.agreement_hash.length} chars`);
226
+ }
227
+ const buyerPubkey = new PublicKey(params.buyerKeypair.publicKey);
228
+ const sellerPubkey = new PublicKey(params.sellerPublicKey);
229
+ const mintPubkey = new PublicKey(params.mintAddress);
230
+ const buyerTokenPubkey = new PublicKey(params.buyerTokenAccount);
231
+ const { address: escrowAddress, bump: _escrowBump } = this.deriveEscrowAddress(params.buyerKeypair.publicKey, hashBytes);
232
+ const escrowPubkey = new PublicKey(escrowAddress);
233
+ const { address: vaultAddress } = this.deriveVaultAddress(escrowPubkey.toBytes());
234
+ const vaultPubkey = new PublicKey(vaultAddress);
235
+ const instructionData = serializeMakeEscrow(hashBytes, params.depositAmount, BigInt(timeoutSlots), penaltyRateBps);
236
+ const instruction = new TransactionInstruction({
237
+ keys: [
238
+ { pubkey: buyerPubkey, isSigner: true, isWritable: true },
239
+ { pubkey: sellerPubkey, isSigner: false, isWritable: false },
240
+ { pubkey: escrowPubkey, isSigner: false, isWritable: true },
241
+ { pubkey: vaultPubkey, isSigner: false, isWritable: true },
242
+ { pubkey: buyerTokenPubkey, isSigner: false, isWritable: true },
243
+ { pubkey: mintPubkey, isSigner: false, isWritable: false },
244
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
245
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
246
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
247
+ ],
248
+ programId: this.programId,
249
+ data: instructionData,
250
+ });
251
+ const connection = new Connection(this.rpcUrl, 'confirmed');
252
+ const buyerSigner = Keypair.fromSecretKey(params.buyerKeypair.secretKey);
253
+ const transaction = new Transaction().add(instruction);
254
+ try {
255
+ const txSignature = await sendAndConfirmTransaction(connection, transaction, [buyerSigner]);
256
+ return { escrowAddress, vaultAddress, txSignature };
257
+ }
258
+ catch (err) {
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ throw new OphirError(OphirErrorCode.ESCROW_CREATION_FAILED, `Failed to create escrow: ${message}`, { escrowAddress, vaultAddress });
261
+ }
262
+ }
263
+ /**
264
+ * Release escrowed funds to the seller after successful job completion.
265
+ *
266
+ * Only the seller can call this instruction. Transfers the entire vault
267
+ * balance to the seller's token account and marks the escrow as Released.
268
+ *
269
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
270
+ * @param params.sellerKeypair - Seller's Ed25519 keypair (must match escrow.seller)
271
+ * @param params.sellerTokenAccount - Seller's token account to receive funds
272
+ * @param params.verificationHash - Optional 32-byte proof of service delivery
273
+ * @returns The transaction signature
274
+ * @throws {OphirError} ESCROW_ALREADY_RELEASED if the escrow is not Active
275
+ */
276
+ async releaseEscrow(params) {
277
+ if (params.verificationHash && params.verificationHash.length !== 32) {
278
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid verification hash length: expected 32, got ${params.verificationHash.length}`);
279
+ }
280
+ const escrowPubkey = new PublicKey(params.escrowAddress);
281
+ const sellerPubkey = new PublicKey(params.sellerKeypair.publicKey);
282
+ const sellerTokenPubkey = new PublicKey(params.sellerTokenAccount);
283
+ const { address: vaultAddress } = this.deriveVaultAddress(escrowPubkey.toBytes());
284
+ const vaultPubkey = new PublicKey(vaultAddress);
285
+ const instructionData = serializeReleaseEscrow(params.verificationHash);
286
+ const instruction = new TransactionInstruction({
287
+ keys: [
288
+ { pubkey: sellerPubkey, isSigner: true, isWritable: true },
289
+ { pubkey: escrowPubkey, isSigner: false, isWritable: true },
290
+ { pubkey: vaultPubkey, isSigner: false, isWritable: true },
291
+ { pubkey: sellerTokenPubkey, isSigner: false, isWritable: true },
292
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
293
+ ],
294
+ programId: this.programId,
295
+ data: instructionData,
296
+ });
297
+ const connection = new Connection(this.rpcUrl, 'confirmed');
298
+ const sellerSigner = Keypair.fromSecretKey(params.sellerKeypair.secretKey);
299
+ const transaction = new Transaction().add(instruction);
300
+ try {
301
+ const txSignature = await sendAndConfirmTransaction(connection, transaction, [sellerSigner]);
302
+ return { txSignature };
303
+ }
304
+ catch (err) {
305
+ const message = err instanceof Error ? err.message : String(err);
306
+ throw new OphirError(OphirErrorCode.ESCROW_ALREADY_RELEASED, `Failed to release escrow: ${message}`, { escrowAddress: params.escrowAddress });
307
+ }
308
+ }
309
+ /**
310
+ * File an on-chain dispute, splitting funds between buyer (penalty) and seller (remainder).
311
+ *
312
+ * Only the buyer can initiate a dispute. The penalty amount must not exceed
313
+ * `deposit_amount * penalty_rate_bps / 10000`. The penalty is returned to
314
+ * the buyer and the remainder goes to the seller.
315
+ *
316
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
317
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (must match escrow.buyer)
318
+ * @param params.buyerTokenAccount - Buyer's token account for penalty refund
319
+ * @param params.sellerTokenAccount - Seller's token account for remainder
320
+ * @param params.evidenceHash - 32-byte SHA-256 hash of the violation evidence
321
+ * @param params.penaltyAmount - Penalty in smallest token units
322
+ * @returns The transaction signature
323
+ * @throws {OphirError} ESCROW_VERIFICATION_FAILED if penalty exceeds max allowed
324
+ */
325
+ async disputeEscrow(params) {
326
+ if (params.evidenceHash.length !== 32) {
327
+ throw new OphirError(OphirErrorCode.DISPUTE_INVALID_EVIDENCE, `Invalid evidence hash length: expected 32, got ${params.evidenceHash.length}`);
328
+ }
329
+ if (params.penaltyAmount < 0n) {
330
+ throw new OphirError(OphirErrorCode.ESCROW_VERIFICATION_FAILED, 'Penalty amount cannot be negative');
331
+ }
332
+ const escrowPubkey = new PublicKey(params.escrowAddress);
333
+ const buyerPubkey = new PublicKey(params.buyerKeypair.publicKey);
334
+ const buyerTokenPubkey = new PublicKey(params.buyerTokenAccount);
335
+ const sellerTokenPubkey = new PublicKey(params.sellerTokenAccount);
336
+ const { address: vaultAddress } = this.deriveVaultAddress(escrowPubkey.toBytes());
337
+ const vaultPubkey = new PublicKey(vaultAddress);
338
+ const instructionData = serializeDisputeEscrow(params.evidenceHash, params.penaltyAmount);
339
+ const instruction = new TransactionInstruction({
340
+ keys: [
341
+ { pubkey: buyerPubkey, isSigner: true, isWritable: true },
342
+ { pubkey: escrowPubkey, isSigner: false, isWritable: true },
343
+ { pubkey: vaultPubkey, isSigner: false, isWritable: true },
344
+ { pubkey: buyerTokenPubkey, isSigner: false, isWritable: true },
345
+ { pubkey: sellerTokenPubkey, isSigner: false, isWritable: true },
346
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
347
+ ],
348
+ programId: this.programId,
349
+ data: instructionData,
350
+ });
351
+ const connection = new Connection(this.rpcUrl, 'confirmed');
352
+ const buyerSigner = Keypair.fromSecretKey(params.buyerKeypair.secretKey);
353
+ const transaction = new Transaction().add(instruction);
354
+ try {
355
+ const txSignature = await sendAndConfirmTransaction(connection, transaction, [buyerSigner]);
356
+ return { txSignature };
357
+ }
358
+ catch (err) {
359
+ const message = err instanceof Error ? err.message : String(err);
360
+ throw new OphirError(OphirErrorCode.ESCROW_VERIFICATION_FAILED, `Failed to dispute escrow: ${message}`, { escrowAddress: params.escrowAddress });
361
+ }
362
+ }
363
+ /**
364
+ * Cancel an escrow after the timeout slot has passed, returning all funds to the buyer.
365
+ *
366
+ * Only the buyer can cancel, and only after the escrow's timeout_slot has been reached.
367
+ * Transfers the entire vault balance back to the buyer's token account.
368
+ *
369
+ * @param params.escrowAddress - Base58-encoded escrow PDA address
370
+ * @param params.buyerKeypair - Buyer's Ed25519 keypair (must match escrow.buyer)
371
+ * @param params.buyerTokenAccount - Buyer's token account for refund
372
+ * @returns The transaction signature
373
+ * @throws {OphirError} ESCROW_TIMEOUT_NOT_REACHED if timeout has not elapsed
374
+ */
375
+ async cancelEscrow(params) {
376
+ const escrowPubkey = new PublicKey(params.escrowAddress);
377
+ const buyerPubkey = new PublicKey(params.buyerKeypair.publicKey);
378
+ const buyerTokenPubkey = new PublicKey(params.buyerTokenAccount);
379
+ const { address: vaultAddress } = this.deriveVaultAddress(escrowPubkey.toBytes());
380
+ const vaultPubkey = new PublicKey(vaultAddress);
381
+ const instructionData = serializeCancelEscrow();
382
+ const instruction = new TransactionInstruction({
383
+ keys: [
384
+ { pubkey: buyerPubkey, isSigner: true, isWritable: true },
385
+ { pubkey: escrowPubkey, isSigner: false, isWritable: true },
386
+ { pubkey: vaultPubkey, isSigner: false, isWritable: true },
387
+ { pubkey: buyerTokenPubkey, isSigner: false, isWritable: true },
388
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
389
+ ],
390
+ programId: this.programId,
391
+ data: instructionData,
392
+ });
393
+ const connection = new Connection(this.rpcUrl, 'confirmed');
394
+ const buyerSigner = Keypair.fromSecretKey(params.buyerKeypair.secretKey);
395
+ const transaction = new Transaction().add(instruction);
396
+ try {
397
+ const txSignature = await sendAndConfirmTransaction(connection, transaction, [buyerSigner]);
398
+ return { txSignature };
399
+ }
400
+ catch (err) {
401
+ const message = err instanceof Error ? err.message : String(err);
402
+ throw new OphirError(OphirErrorCode.ESCROW_TIMEOUT_NOT_REACHED, `Failed to cancel escrow: ${message}`, { escrowAddress: params.escrowAddress });
403
+ }
404
+ }
405
+ /**
406
+ * Fetch and deserialize escrow account data from Solana.
407
+ *
408
+ * Connects to the configured Solana RPC endpoint, fetches the raw account
409
+ * data at the given address, and deserializes it into a typed EscrowAccountData.
410
+ *
411
+ * @param escrowAddress - Base58-encoded escrow PDA address
412
+ * @returns The deserialized escrow account data
413
+ * @throws {OphirError} SOLANA_RPC_ERROR if the account doesn't exist or deserialization fails
414
+ */
415
+ async getEscrowStatus(escrowAddress) {
416
+ const connection = new Connection(this.rpcUrl, 'confirmed');
417
+ const pubkey = new PublicKey(escrowAddress);
418
+ let accountInfo;
419
+ try {
420
+ accountInfo = await connection.getAccountInfo(pubkey);
421
+ }
422
+ catch (err) {
423
+ const message = err instanceof Error ? err.message : String(err);
424
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, `Failed to fetch escrow account: ${message}`, { escrowAddress });
425
+ }
426
+ if (!accountInfo) {
427
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, `Escrow account not found at address: ${escrowAddress}`, { escrowAddress });
428
+ }
429
+ if (!accountInfo.owner.equals(this.programId)) {
430
+ throw new OphirError(OphirErrorCode.SOLANA_RPC_ERROR, `Account at ${escrowAddress} is not owned by the Ophir escrow program`, { escrowAddress, owner: accountInfo.owner.toBase58() });
431
+ }
432
+ return deserializeEscrowAccount(Buffer.from(accountInfo.data));
433
+ }
434
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Generate an Ed25519 keypair for agent identity.
3
+ *
4
+ * @throws {OphirError} if the generated keypair has unexpected key lengths.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const { publicKey, secretKey } = generateKeyPair();
9
+ * // publicKey: Uint8Array(32), secretKey: Uint8Array(64)
10
+ * ```
11
+ */
12
+ export declare function generateKeyPair(): {
13
+ publicKey: Uint8Array;
14
+ secretKey: Uint8Array;
15
+ };
16
+ /**
17
+ * Convert Ed25519 public key to did:key:z6Mk... format.
18
+ * Prepends multicodec prefix (0xed01) then base58-btc encodes with 'z' prefix.
19
+ *
20
+ * @throws {OphirError} if publicKey is not exactly 32 bytes.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const did = publicKeyToDid(keypair.publicKey);
25
+ * // 'did:key:z6Mk...'
26
+ * ```
27
+ */
28
+ export declare function publicKeyToDid(publicKey: Uint8Array): string;
29
+ /**
30
+ * Extract Ed25519 public key from did:key string.
31
+ *
32
+ * @throws {OphirError} if input is empty, DID format is invalid, or extracted key is not 32 bytes.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const publicKey = didToPublicKey('did:key:z6Mk...');
37
+ * // Uint8Array(32)
38
+ * ```
39
+ */
40
+ export declare function didToPublicKey(did: string): Uint8Array;
41
+ /**
42
+ * Generate a complete agent identity bundle.
43
+ *
44
+ * @param endpoint - The HTTPS endpoint URL for the agent.
45
+ * @throws {OphirError} if endpoint is not a valid URL with http or https protocol.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const identity = generateAgentIdentity('https://agent.example.com');
50
+ * // { agentId: 'did:key:z6Mk...', keypair: { publicKey, secretKey }, endpoint: 'https://...' }
51
+ * ```
52
+ */
53
+ export declare function generateAgentIdentity(endpoint: string): {
54
+ agentId: string;
55
+ keypair: {
56
+ publicKey: Uint8Array;
57
+ secretKey: Uint8Array;
58
+ };
59
+ endpoint: string;
60
+ };