@agirails/sdk 2.0.0-beta
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.
- package/README.md +183 -0
- package/dist/ACTPClient.d.ts +52 -0
- package/dist/ACTPClient.d.ts.map +1 -0
- package/dist/ACTPClient.js +120 -0
- package/dist/ACTPClient.js.map +1 -0
- package/dist/abi/ACTPKernel.json +1340 -0
- package/dist/abi/ERC20.json +38 -0
- package/dist/abi/EscrowVault.json +64 -0
- package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
- package/dist/builders/DeliveryProofBuilder.js +165 -0
- package/dist/builders/DeliveryProofBuilder.js.map +1 -0
- package/dist/builders/QuoteBuilder.d.ts +68 -0
- package/dist/builders/QuoteBuilder.d.ts.map +1 -0
- package/dist/builders/QuoteBuilder.js +255 -0
- package/dist/builders/QuoteBuilder.js.map +1 -0
- package/dist/builders/index.d.ts +3 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +10 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/config/networks.d.ts +27 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +103 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/errors/index.d.ts +38 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +87 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ACTPKernel.d.ts +30 -0
- package/dist/protocol/ACTPKernel.d.ts.map +1 -0
- package/dist/protocol/ACTPKernel.js +261 -0
- package/dist/protocol/ACTPKernel.js.map +1 -0
- package/dist/protocol/EASHelper.d.ts +23 -0
- package/dist/protocol/EASHelper.d.ts.map +1 -0
- package/dist/protocol/EASHelper.js +106 -0
- package/dist/protocol/EASHelper.js.map +1 -0
- package/dist/protocol/EscrowVault.d.ts +24 -0
- package/dist/protocol/EscrowVault.d.ts.map +1 -0
- package/dist/protocol/EscrowVault.js +114 -0
- package/dist/protocol/EscrowVault.js.map +1 -0
- package/dist/protocol/EventMonitor.d.ts +18 -0
- package/dist/protocol/EventMonitor.d.ts.map +1 -0
- package/dist/protocol/EventMonitor.js +92 -0
- package/dist/protocol/EventMonitor.js.map +1 -0
- package/dist/protocol/MessageSigner.d.ts +23 -0
- package/dist/protocol/MessageSigner.d.ts.map +1 -0
- package/dist/protocol/MessageSigner.js +178 -0
- package/dist/protocol/MessageSigner.js.map +1 -0
- package/dist/protocol/ProofGenerator.d.ts +22 -0
- package/dist/protocol/ProofGenerator.d.ts.map +1 -0
- package/dist/protocol/ProofGenerator.js +64 -0
- package/dist/protocol/ProofGenerator.js.map +1 -0
- package/dist/protocol/QuoteBuilder.d.ts +2 -0
- package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
- package/dist/protocol/QuoteBuilder.js +7 -0
- package/dist/protocol/QuoteBuilder.js.map +1 -0
- package/dist/types/eip712.d.ts +106 -0
- package/dist/types/eip712.d.ts.map +1 -0
- package/dist/types/eip712.js +84 -0
- package/dist/types/eip712.js.map +1 -0
- package/dist/types/escrow.d.ts +18 -0
- package/dist/types/escrow.d.ts.map +1 -0
- package/dist/types/escrow.js +3 -0
- package/dist/types/escrow.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +109 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/state.d.ts +19 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +49 -0
- package/dist/types/state.js.map +1 -0
- package/dist/types/transaction.d.ts +36 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +3 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/utils/IPFSClient.d.ts +37 -0
- package/dist/utils/IPFSClient.d.ts.map +1 -0
- package/dist/utils/IPFSClient.js +128 -0
- package/dist/utils/IPFSClient.js.map +1 -0
- package/dist/utils/NonceManager.d.ts +34 -0
- package/dist/utils/NonceManager.d.ts.map +1 -0
- package/dist/utils/NonceManager.js +114 -0
- package/dist/utils/NonceManager.js.map +1 -0
- package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
- package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
- package/dist/utils/ReceivedNonceTracker.js +196 -0
- package/dist/utils/ReceivedNonceTracker.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +4 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +21 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/computeTypeHash.d.ts +3 -0
- package/dist/utils/computeTypeHash.d.ts.map +1 -0
- package/dist/utils/computeTypeHash.js +30 -0
- package/dist/utils/computeTypeHash.js.map +1 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +46 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +73 -0
- package/src/ACTPClient.ts +276 -0
- package/src/__tests__/ProofGenerator.test.ts +124 -0
- package/src/__tests__/QuoteBuilder.test.ts +516 -0
- package/src/__tests__/StateMachine.test.ts +82 -0
- package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
- package/src/__tests__/integration/ACTPClient.test.ts +263 -0
- package/src/__tests__/integration.test.ts +289 -0
- package/src/__tests__/protocol/EASHelper.test.ts +472 -0
- package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
- package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
- package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
- package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
- package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
- package/src/__tests__/security/validation.security.test.ts +376 -0
- package/src/__tests__/utils/IPFSClient.test.ts +262 -0
- package/src/__tests__/utils/NonceManager.test.ts +205 -0
- package/src/__tests__/utils/canonicalJson.test.ts +153 -0
- package/src/abi/ACTPKernel.json +1340 -0
- package/src/abi/ERC20.json +40 -0
- package/src/abi/EscrowVault.json +66 -0
- package/src/builders/DeliveryProofBuilder.ts +326 -0
- package/src/builders/QuoteBuilder.ts +483 -0
- package/src/builders/index.ts +17 -0
- package/src/config/networks.ts +165 -0
- package/src/errors/index.ts +130 -0
- package/src/index.ts +108 -0
- package/src/protocol/ACTPKernel.ts +625 -0
- package/src/protocol/EASHelper.ts +197 -0
- package/src/protocol/EscrowVault.ts +237 -0
- package/src/protocol/EventMonitor.ts +161 -0
- package/src/protocol/MessageSigner.ts +336 -0
- package/src/protocol/ProofGenerator.ts +119 -0
- package/src/protocol/QuoteBuilder.ts +15 -0
- package/src/types/eip712.ts +175 -0
- package/src/types/escrow.ts +26 -0
- package/src/types/index.ts +10 -0
- package/src/types/message.ts +145 -0
- package/src/types/state.ts +77 -0
- package/src/types/transaction.ts +54 -0
- package/src/utils/IPFSClient.ts +248 -0
- package/src/utils/NonceManager.ts +293 -0
- package/src/utils/ReceivedNonceTracker.ts +397 -0
- package/src/utils/canonicalJson.ts +38 -0
- package/src/utils/computeTypeHash.ts +50 -0
- package/src/utils/validation.ts +82 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { Contract, Signer, BytesLike, ethers, AbiCoder } from 'ethers';
|
|
2
|
+
import ACTPKernelABI from '../abi/ACTPKernel.json';
|
|
3
|
+
import {
|
|
4
|
+
State,
|
|
5
|
+
StateMachine,
|
|
6
|
+
Transaction,
|
|
7
|
+
CreateTransactionParams,
|
|
8
|
+
DisputeResolution,
|
|
9
|
+
EconomicParams
|
|
10
|
+
} from '../types';
|
|
11
|
+
import {
|
|
12
|
+
TransactionNotFoundError,
|
|
13
|
+
TransactionRevertedError,
|
|
14
|
+
InvalidStateTransitionError,
|
|
15
|
+
ValidationError
|
|
16
|
+
} from '../errors';
|
|
17
|
+
import {
|
|
18
|
+
validateAddress,
|
|
19
|
+
validateAmount,
|
|
20
|
+
validateDeadline,
|
|
21
|
+
validateDisputeWindow,
|
|
22
|
+
validateTxId
|
|
23
|
+
} from '../utils/validation';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gas options for transactions
|
|
27
|
+
*/
|
|
28
|
+
interface GasOptions {
|
|
29
|
+
maxFeePerGas?: bigint;
|
|
30
|
+
maxPriorityFeePerGas?: bigint;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ACTPKernel - Smart contract wrapper
|
|
35
|
+
* Reference: Yellow Paper §3 (ACTP Kernel Specification)
|
|
36
|
+
*/
|
|
37
|
+
export class ACTPKernel {
|
|
38
|
+
private contract: Contract;
|
|
39
|
+
private readonly gasSettings?: GasOptions;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly address: string,
|
|
43
|
+
signer: Signer,
|
|
44
|
+
gasSettings?: GasOptions
|
|
45
|
+
) {
|
|
46
|
+
this.contract = new Contract(address, ACTPKernelABI, signer);
|
|
47
|
+
this.gasSettings = gasSettings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get kernel contract address
|
|
52
|
+
*/
|
|
53
|
+
getAddress(): string {
|
|
54
|
+
return this.address;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get gas buffer multiplier based on operation complexity
|
|
59
|
+
* V6 Security Enhancement: Operation-specific gas buffers
|
|
60
|
+
* Reference: SDK_SECURITY_ANALYSIS-Ultra-Think.md Lines 326-337
|
|
61
|
+
*/
|
|
62
|
+
private getGasBufferMultiplier(operation: string): number {
|
|
63
|
+
const buffers: Record<string, number> = {
|
|
64
|
+
'createTransaction': 1.15, // 15% - Simple state initialization
|
|
65
|
+
'transitionState': 1.20, // 20% - Standard state change
|
|
66
|
+
'releaseEscrow': 1.30, // 30% - Multi-recipient disbursement
|
|
67
|
+
'raiseDispute': 1.25, // 25% - Large proof data handling
|
|
68
|
+
'resolveDispute': 1.30, // 30% - Complex multi-party settlement
|
|
69
|
+
'cancelTransaction': 1.15, // 15% - Simple state change
|
|
70
|
+
'anchorAttestation': 1.15 // 15% - Simple attestation anchoring
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return buffers[operation] || 1.20; // Default 20% for unknown operations
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build transaction options with gas settings and estimated gas
|
|
78
|
+
* V6 Enhancement: Dynamic buffer based on operation type
|
|
79
|
+
*/
|
|
80
|
+
private buildTxOptions(estimatedGas: bigint, operation: string = 'default'): any {
|
|
81
|
+
const bufferMultiplier = this.getGasBufferMultiplier(operation);
|
|
82
|
+
|
|
83
|
+
const options: any = {
|
|
84
|
+
gasLimit: (estimatedGas * BigInt(Math.round(bufferMultiplier * 100))) / 100n
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (this.gasSettings?.maxFeePerGas) {
|
|
88
|
+
options.maxFeePerGas = this.gasSettings.maxFeePerGas;
|
|
89
|
+
}
|
|
90
|
+
if (this.gasSettings?.maxPriorityFeePerGas) {
|
|
91
|
+
options.maxPriorityFeePerGas = this.gasSettings.maxPriorityFeePerGas;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return options;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a new transaction
|
|
99
|
+
* Reference: Yellow Paper §3.4.1
|
|
100
|
+
*
|
|
101
|
+
* Contract signature: createTransaction(provider, requester, amount, deadline, disputeWindow, serviceHash)
|
|
102
|
+
* Returns: bytes32 transactionId (generated by contract)
|
|
103
|
+
*/
|
|
104
|
+
async createTransaction(params: CreateTransactionParams): Promise<string> {
|
|
105
|
+
const {
|
|
106
|
+
provider,
|
|
107
|
+
requester,
|
|
108
|
+
amount,
|
|
109
|
+
deadline,
|
|
110
|
+
disputeWindow,
|
|
111
|
+
metadata = '0x0000000000000000000000000000000000000000000000000000000000000000'
|
|
112
|
+
} = params;
|
|
113
|
+
|
|
114
|
+
// Input validation
|
|
115
|
+
validateAddress(provider, 'provider');
|
|
116
|
+
validateAddress(requester, 'requester');
|
|
117
|
+
validateAmount(amount, 'amount');
|
|
118
|
+
validateDeadline(deadline, 'deadline');
|
|
119
|
+
validateDisputeWindow(disputeWindow, 'disputeWindow');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// ethers v6: use getFunction() for typed access
|
|
123
|
+
const createTxFunc = this.contract.getFunction('createTransaction');
|
|
124
|
+
|
|
125
|
+
// Contract signature: createTransaction(provider, requester, amount, deadline, disputeWindow, serviceHash)
|
|
126
|
+
const estimatedGas = await createTxFunc.estimateGas(
|
|
127
|
+
provider,
|
|
128
|
+
requester,
|
|
129
|
+
amount,
|
|
130
|
+
deadline,
|
|
131
|
+
disputeWindow,
|
|
132
|
+
metadata // serviceHash
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Build tx options with gas settings (15% buffer for simple state initialization)
|
|
136
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'createTransaction');
|
|
137
|
+
|
|
138
|
+
// Per ABI: createTransaction returns transactionId directly
|
|
139
|
+
// Contract signature: function createTransaction(...) external returns (bytes32 transactionId)
|
|
140
|
+
const tx = await createTxFunc(
|
|
141
|
+
provider,
|
|
142
|
+
requester,
|
|
143
|
+
amount,
|
|
144
|
+
deadline,
|
|
145
|
+
disputeWindow,
|
|
146
|
+
metadata, // serviceHash
|
|
147
|
+
txOptions
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const receipt = await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
151
|
+
if (!receipt) {
|
|
152
|
+
throw new Error('Transaction receipt not available');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Extract transactionId from TransactionCreated event
|
|
156
|
+
// Event signature: TransactionCreated(bytes32 indexed transactionId, ...)
|
|
157
|
+
for (const log of receipt.logs) {
|
|
158
|
+
try {
|
|
159
|
+
const parsedLog = this.contract.interface.parseLog({
|
|
160
|
+
topics: [...log.topics],
|
|
161
|
+
data: log.data
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (parsedLog && parsedLog.name === 'TransactionCreated') {
|
|
165
|
+
return parsedLog.args.transactionId || parsedLog.args[0];
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// Skip logs that don't match our interface
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new Error('TransactionCreated event not found in receipt');
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Transition transaction state
|
|
181
|
+
* Reference: Yellow Paper §3.2
|
|
182
|
+
*/
|
|
183
|
+
async transitionState(
|
|
184
|
+
txId: string,
|
|
185
|
+
newState: State,
|
|
186
|
+
proof: BytesLike = '0x'
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
// Input validation
|
|
189
|
+
validateTxId(txId, 'txId');
|
|
190
|
+
|
|
191
|
+
// Validate transition
|
|
192
|
+
const currentTx = await this.getTransaction(txId);
|
|
193
|
+
if (!StateMachine.isValidTransition(currentTx.state, newState)) {
|
|
194
|
+
const validStates = StateMachine.getNextValidStates(currentTx.state).map((s) =>
|
|
195
|
+
State[s]
|
|
196
|
+
);
|
|
197
|
+
throw new InvalidStateTransitionError(currentTx.state, newState, validStates);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// ethers v6: use getFunction()
|
|
202
|
+
const transitionFunc = this.contract.getFunction('transitionState');
|
|
203
|
+
|
|
204
|
+
// Estimate gas with safety buffer (20% for standard state transitions)
|
|
205
|
+
const estimatedGas = await transitionFunc.estimateGas(txId, newState, proof);
|
|
206
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'transitionState');
|
|
207
|
+
|
|
208
|
+
const tx = await transitionFunc(txId, newState, proof, txOptions);
|
|
209
|
+
|
|
210
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Submit quote for transaction (AIP-2)
|
|
218
|
+
* Reference: AIP-2 §4.1 (Provider workflow)
|
|
219
|
+
*
|
|
220
|
+
* Transitions transaction from INITIATED → QUOTED with quote hash stored on-chain
|
|
221
|
+
*
|
|
222
|
+
* @param txId - Transaction ID (bytes32)
|
|
223
|
+
* @param quoteHash - Keccak256 hash of canonical JSON quote message
|
|
224
|
+
*/
|
|
225
|
+
async submitQuote(txId: string, quoteHash: string): Promise<void> {
|
|
226
|
+
// Input validation
|
|
227
|
+
validateTxId(txId, 'txId');
|
|
228
|
+
|
|
229
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(quoteHash)) {
|
|
230
|
+
throw new ValidationError('quoteHash', 'Must be valid bytes32 hex string');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (quoteHash === '0x0000000000000000000000000000000000000000000000000000000000000000') {
|
|
234
|
+
throw new ValidationError('quoteHash', 'Cannot be zero hash');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate current state is INITIATED
|
|
238
|
+
const currentTx = await this.getTransaction(txId);
|
|
239
|
+
if (currentTx.state !== State.INITIATED) {
|
|
240
|
+
throw new InvalidStateTransitionError(
|
|
241
|
+
currentTx.state,
|
|
242
|
+
State.QUOTED,
|
|
243
|
+
['INITIATED']
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Encode quote hash as bytes proof
|
|
248
|
+
const abiCoder = AbiCoder.defaultAbiCoder();
|
|
249
|
+
const proof = abiCoder.encode(['bytes32'], [quoteHash]);
|
|
250
|
+
|
|
251
|
+
// Transition to QUOTED state with quote hash
|
|
252
|
+
await this.transitionState(txId, State.QUOTED, proof);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Link escrow to transaction
|
|
257
|
+
*
|
|
258
|
+
* CRITICAL: This is the ONLY way to create escrow per AIP-3 spec.
|
|
259
|
+
* SDK should NOT call EscrowVault.createEscrow() directly (onlyKernel modifier).
|
|
260
|
+
*
|
|
261
|
+
* What happens internally:
|
|
262
|
+
* 1. ACTPKernel validates transaction state, permissions, deadline
|
|
263
|
+
* 2. Kernel calls IEscrowValidator(escrowContract).createEscrow(...)
|
|
264
|
+
* 3. EscrowVault pulls USDC from consumer (must approve USDC first!)
|
|
265
|
+
* 4. Events emitted: EscrowLinked
|
|
266
|
+
* 5. **State transition behavior varies** (see below)
|
|
267
|
+
*
|
|
268
|
+
* STATE TRANSITION BEHAVIOR:
|
|
269
|
+
* - **AIP-3 Spec (Source Code)**: Should auto-transition INITIATED/QUOTED → COMMITTED
|
|
270
|
+
* - **Deployed Contract**: Behavior is INCONSISTENT - sometimes auto-transitions, sometimes doesn't
|
|
271
|
+
* - **Recommended Practice**: Always check state after linkEscrow() and manually transition if needed
|
|
272
|
+
*
|
|
273
|
+
* ```typescript
|
|
274
|
+
* await client.kernel.linkEscrow(txId, escrowVault, escrowId);
|
|
275
|
+
* let tx = await client.kernel.getTransaction(txId);
|
|
276
|
+
* if (tx.state !== State.COMMITTED) {
|
|
277
|
+
* await client.kernel.transitionState(txId, State.COMMITTED);
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* Prerequisites:
|
|
282
|
+
* - Transaction in INITIATED or QUOTED state
|
|
283
|
+
* - Consumer has approved USDC to EscrowVault address
|
|
284
|
+
* (use EscrowVault.approveToken() before calling this)
|
|
285
|
+
*
|
|
286
|
+
* @param txId - Transaction ID (bytes32)
|
|
287
|
+
* @param escrowContract - EscrowVault contract address
|
|
288
|
+
* @param escrowId - Unique escrow identifier (bytes32, consumer-generated)
|
|
289
|
+
* @throws {ValidationError} If inputs invalid
|
|
290
|
+
* @throws {TransactionRevertedError} If state invalid, deadline passed, or insufficient USDC
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // Step 1: Approve USDC to EscrowVault (NOT to Kernel!)
|
|
295
|
+
* await client.escrow.approveToken(BASE_SEPOLIA.contracts.usdc, amount);
|
|
296
|
+
*
|
|
297
|
+
* // Step 2: Generate unique escrow ID
|
|
298
|
+
* const escrowId = ethers.id(`escrow-${Date.now()}`);
|
|
299
|
+
*
|
|
300
|
+
* // Step 3: Link escrow (creates escrow + auto-transitions to COMMITTED)
|
|
301
|
+
* await client.kernel.linkEscrow(txId, escrowVaultAddress, escrowId);
|
|
302
|
+
*
|
|
303
|
+
* // Step 4: Verify state is COMMITTED (auto-transitioned, no manual call needed)
|
|
304
|
+
* const tx = await client.kernel.getTransaction(txId);
|
|
305
|
+
* expect(tx.state).to.equal(State.COMMITTED);
|
|
306
|
+
* ```
|
|
307
|
+
*
|
|
308
|
+
* Reference: Yellow Paper §3.4.2, AIP-3 §3.2 (ACTPKernel.sol lines 244-276)
|
|
309
|
+
*/
|
|
310
|
+
async linkEscrow(
|
|
311
|
+
txId: string,
|
|
312
|
+
escrowContract: string,
|
|
313
|
+
escrowId: string
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
// Input validation
|
|
316
|
+
validateTxId(txId, 'txId');
|
|
317
|
+
validateAddress(escrowContract, 'escrowContract');
|
|
318
|
+
validateTxId(escrowId, 'escrowId'); // escrowId is also bytes32
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
// ethers v6: use getFunction()
|
|
322
|
+
const linkEscrowFunc = this.contract.getFunction('linkEscrow');
|
|
323
|
+
|
|
324
|
+
// Estimate gas with safety buffer (20% for linking escrow)
|
|
325
|
+
const estimatedGas = await linkEscrowFunc.estimateGas(txId, escrowContract, escrowId);
|
|
326
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'transitionState');
|
|
327
|
+
|
|
328
|
+
const tx = await linkEscrowFunc(txId, escrowContract, escrowId, txOptions);
|
|
329
|
+
|
|
330
|
+
// Wait for 2 confirmations to ensure state is updated on RPC nodes (Base Sepolia reorg safety)
|
|
331
|
+
await tx.wait(2);
|
|
332
|
+
} catch (error: any) {
|
|
333
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Release milestone payment
|
|
339
|
+
*/
|
|
340
|
+
async releaseMilestone(
|
|
341
|
+
txId: string,
|
|
342
|
+
milestoneId: number,
|
|
343
|
+
amount: bigint
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
// Input validation
|
|
346
|
+
validateTxId(txId, 'txId');
|
|
347
|
+
validateAmount(amount, 'amount');
|
|
348
|
+
if (milestoneId < 0) {
|
|
349
|
+
throw new ValidationError('milestoneId', 'Milestone ID cannot be negative');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// ethers v6: use getFunction()
|
|
354
|
+
const releaseMilestoneFunc = this.contract.getFunction('releaseMilestone');
|
|
355
|
+
|
|
356
|
+
// Estimate gas with safety buffer (30% for escrow release operations)
|
|
357
|
+
const estimatedGas = await releaseMilestoneFunc.estimateGas(txId, milestoneId, amount);
|
|
358
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'releaseEscrow');
|
|
359
|
+
|
|
360
|
+
const tx = await releaseMilestoneFunc(txId, milestoneId, amount, txOptions);
|
|
361
|
+
|
|
362
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
363
|
+
} catch (error: any) {
|
|
364
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Release full escrow (settle transaction)
|
|
370
|
+
*
|
|
371
|
+
* SECURITY WARNING (V1): ACTPKernel V1 contract accepts any attestationUID without validation.
|
|
372
|
+
* This means a malicious provider could submit attestation from different transaction.
|
|
373
|
+
*
|
|
374
|
+
* To protect against this, you can either:
|
|
375
|
+
* 1. Use ACTPClient's wrapper method that automatically verifies
|
|
376
|
+
* 2. Manually verify attestation before calling this method (see EASHelper.verifyDeliveryAttestation)
|
|
377
|
+
*
|
|
378
|
+
* Note: Attestation verification is OPTIONAL here because some transactions may not use EAS.
|
|
379
|
+
* However, for transactions with delivery proofs, consumers SHOULD verify before settling.
|
|
380
|
+
*
|
|
381
|
+
* @param txId - Transaction ID to settle
|
|
382
|
+
* @throws {ValidationError} If txId is invalid
|
|
383
|
+
* @throws {TransactionRevertedError} If contract reverts
|
|
384
|
+
*/
|
|
385
|
+
async releaseEscrow(txId: string): Promise<void> {
|
|
386
|
+
// Input validation
|
|
387
|
+
validateTxId(txId, 'txId');
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
// ethers v6: use getFunction()
|
|
391
|
+
const releaseEscrowFunc = this.contract.getFunction('releaseEscrow');
|
|
392
|
+
|
|
393
|
+
// Estimate gas with safety buffer (30% for escrow release operations)
|
|
394
|
+
const estimatedGas = await releaseEscrowFunc.estimateGas(txId);
|
|
395
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'releaseEscrow');
|
|
396
|
+
|
|
397
|
+
const tx = await releaseEscrowFunc(txId, txOptions);
|
|
398
|
+
|
|
399
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
400
|
+
} catch (error: any) {
|
|
401
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get transaction by ID
|
|
407
|
+
*/
|
|
408
|
+
async getTransaction(txId: string): Promise<Transaction> {
|
|
409
|
+
const txData = await this.contract.getTransaction(txId);
|
|
410
|
+
|
|
411
|
+
// Check if transaction exists (createdAt !== 0)
|
|
412
|
+
if (txData.createdAt === 0 || txData.createdAt === 0n) {
|
|
413
|
+
throw new TransactionNotFoundError(txId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
txId: txData.transactionId,
|
|
418
|
+
requester: txData.requester,
|
|
419
|
+
provider: txData.provider,
|
|
420
|
+
amount: txData.amount,
|
|
421
|
+
state: (typeof txData.state === 'bigint' ? Number(txData.state) : txData.state) as State,
|
|
422
|
+
createdAt: typeof txData.createdAt === 'bigint' ? Number(txData.createdAt) : txData.createdAt,
|
|
423
|
+
deadline: typeof txData.deadline === 'bigint' ? Number(txData.deadline) : txData.deadline,
|
|
424
|
+
disputeWindow: typeof txData.disputeWindow === 'bigint' ? Number(txData.disputeWindow) : txData.disputeWindow,
|
|
425
|
+
escrowContract: txData.escrowContract,
|
|
426
|
+
escrowId: txData.escrowId,
|
|
427
|
+
// Use metadata field (quote hash for QUOTED state) if available, fallback to serviceHash
|
|
428
|
+
metadata: txData.metadata || txData.serviceHash
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get economic parameters (fee structure)
|
|
434
|
+
* Fixed: Don't hardcode values, read from contract
|
|
435
|
+
*/
|
|
436
|
+
async getEconomicParams(): Promise<EconomicParams> {
|
|
437
|
+
const params = await this.contract.getEconomicParams();
|
|
438
|
+
|
|
439
|
+
// Contract returns: (platformFeeBps, requesterPenaltyBps, feeRecipient)
|
|
440
|
+
return {
|
|
441
|
+
baseFeeNumerator: Number(params.platformFeeBps || params[0]),
|
|
442
|
+
baseFeeDenominator: 10000, // BPS is always out of 10000
|
|
443
|
+
feeRecipient: params.feeRecipient || params[2],
|
|
444
|
+
requesterPenaltyBps: Number(params.requesterPenaltyBps || params[1]),
|
|
445
|
+
providerPenaltyBps: 0 // Not in current contract ABI, will be added in future version
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Estimate gas for transaction creation
|
|
451
|
+
*/
|
|
452
|
+
async estimateCreateTransaction(params: CreateTransactionParams): Promise<bigint> {
|
|
453
|
+
const { provider, requester, amount, deadline, disputeWindow, metadata = '0x0000000000000000000000000000000000000000000000000000000000000000' } = params;
|
|
454
|
+
|
|
455
|
+
// ethers v6: use getFunction()
|
|
456
|
+
const createTxFunc = this.contract.getFunction('createTransaction');
|
|
457
|
+
return await createTxFunc.estimateGas(
|
|
458
|
+
provider,
|
|
459
|
+
requester,
|
|
460
|
+
amount,
|
|
461
|
+
deadline,
|
|
462
|
+
disputeWindow,
|
|
463
|
+
metadata
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Raise dispute on delivered transaction
|
|
469
|
+
* Reference: Yellow Paper §3.4 (Dispute Management)
|
|
470
|
+
*/
|
|
471
|
+
async raiseDispute(txId: string, reason: string, evidence: string): Promise<void> {
|
|
472
|
+
validateTxId(txId, 'txId');
|
|
473
|
+
|
|
474
|
+
// Encode dispute proof with reason and evidence (IPFS hash)
|
|
475
|
+
const abiCoder = AbiCoder.defaultAbiCoder();
|
|
476
|
+
const proofData = abiCoder.encode(
|
|
477
|
+
['string', 'string'],
|
|
478
|
+
[reason, evidence]
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
// ethers v6: use getFunction()
|
|
483
|
+
const transitionFunc = this.contract.getFunction('transitionState');
|
|
484
|
+
|
|
485
|
+
// Estimate gas with safety buffer (25% for large proof data)
|
|
486
|
+
const estimatedGas = await transitionFunc.estimateGas(
|
|
487
|
+
txId,
|
|
488
|
+
State.DISPUTED,
|
|
489
|
+
proofData
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'raiseDispute');
|
|
493
|
+
|
|
494
|
+
const tx = await transitionFunc(
|
|
495
|
+
txId,
|
|
496
|
+
State.DISPUTED,
|
|
497
|
+
proofData,
|
|
498
|
+
txOptions
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
502
|
+
} catch (error: any) {
|
|
503
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Resolve/settle dispute with payment split
|
|
509
|
+
* Reference: Yellow Paper §3.4
|
|
510
|
+
*
|
|
511
|
+
* Disputes are settled via transitionState(SETTLED, proof) per §3.2
|
|
512
|
+
* The kernel contract decodes the proof and handles escrow disbursement
|
|
513
|
+
*/
|
|
514
|
+
async resolveDispute(txId: string, resolution: DisputeResolution): Promise<void> {
|
|
515
|
+
validateTxId(txId, 'txId');
|
|
516
|
+
|
|
517
|
+
const { requesterAmount, providerAmount, mediatorAmount, mediator } = resolution;
|
|
518
|
+
|
|
519
|
+
// Validate amounts are non-negative
|
|
520
|
+
if (requesterAmount < 0n || providerAmount < 0n || mediatorAmount < 0n) {
|
|
521
|
+
throw new Error('Dispute resolution amounts cannot be negative');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Validate mediator address if mediator amount > 0
|
|
525
|
+
if (mediatorAmount > 0n) {
|
|
526
|
+
if (!mediator) {
|
|
527
|
+
throw new Error('Mediator address required when mediator amount > 0');
|
|
528
|
+
}
|
|
529
|
+
validateAddress(mediator, 'mediator');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Encode resolution proof (128 bytes: 3x uint256 + address)
|
|
533
|
+
// Kernel contract will decode this in _decodeResolutionProof and disburse funds
|
|
534
|
+
const abiCoder = AbiCoder.defaultAbiCoder();
|
|
535
|
+
const proofData = abiCoder.encode(
|
|
536
|
+
['uint256', 'uint256', 'uint256', 'address'],
|
|
537
|
+
[
|
|
538
|
+
requesterAmount,
|
|
539
|
+
providerAmount,
|
|
540
|
+
mediatorAmount,
|
|
541
|
+
mediator || ethers.getAddress('0x0000000000000000000000000000000000000000')
|
|
542
|
+
]
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
// ethers v6: use getFunction()
|
|
547
|
+
const transitionFunc = this.contract.getFunction('transitionState');
|
|
548
|
+
|
|
549
|
+
// Settle dispute via state transition to SETTLED with resolution proof (30% buffer)
|
|
550
|
+
const estimatedGas = await transitionFunc.estimateGas(
|
|
551
|
+
txId,
|
|
552
|
+
State.SETTLED,
|
|
553
|
+
proofData
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'resolveDispute');
|
|
557
|
+
|
|
558
|
+
const tx = await transitionFunc(
|
|
559
|
+
txId,
|
|
560
|
+
State.SETTLED,
|
|
561
|
+
proofData,
|
|
562
|
+
txOptions
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
566
|
+
} catch (error: any) {
|
|
567
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Settle disputed transaction (alias for resolveDispute)
|
|
573
|
+
*/
|
|
574
|
+
async settleDispute(txId: string, resolution: DisputeResolution): Promise<void> {
|
|
575
|
+
return this.resolveDispute(txId, resolution);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Anchor an EAS attestation UID to a transaction (delivery proof)
|
|
580
|
+
* Reference: AIP-4 (Delivery Proof and EAS Attestation Standard)
|
|
581
|
+
*
|
|
582
|
+
* @param txId - Transaction ID
|
|
583
|
+
* @param attestationUID - EAS attestation UID from provider
|
|
584
|
+
* @throws {ValidationError} If inputs are invalid
|
|
585
|
+
* @throws {TransactionRevertedError} If contract reverts
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* const easHelper = new EASHelper(signer, easConfig);
|
|
590
|
+
* const attestation = await easHelper.attestDeliveryProof(proof, recipient);
|
|
591
|
+
* await kernel.anchorAttestation(txId, attestation.uid);
|
|
592
|
+
* ```
|
|
593
|
+
*/
|
|
594
|
+
async anchorAttestation(txId: string, attestationUID: string): Promise<void> {
|
|
595
|
+
validateTxId(txId, 'txId');
|
|
596
|
+
|
|
597
|
+
// Validate attestationUID format (32-byte hex string)
|
|
598
|
+
if (!attestationUID || !/^0x[a-fA-F0-9]{64}$/.test(attestationUID)) {
|
|
599
|
+
throw new ValidationError('attestationUID', 'Must be 32-byte hex string (0x...)');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
// ethers v6: use getFunction()
|
|
604
|
+
const anchorFunc = this.contract.getFunction('anchorAttestation');
|
|
605
|
+
|
|
606
|
+
const estimatedGas = await anchorFunc.estimateGas(
|
|
607
|
+
txId,
|
|
608
|
+
attestationUID
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const txOptions = this.buildTxOptions(estimatedGas, 'anchorAttestation');
|
|
612
|
+
|
|
613
|
+
const tx = await anchorFunc(
|
|
614
|
+
txId,
|
|
615
|
+
attestationUID,
|
|
616
|
+
txOptions
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
await tx.wait(2); // Wait for 2 confirmations (Base L2 reorg safety)
|
|
620
|
+
} catch (error: any) {
|
|
621
|
+
throw new TransactionRevertedError(error.transactionHash, error.reason || error.message);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
}
|