@inco/shield-js 0.0.0-bootstrap.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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +359 -0
  3. package/dist/binary.d.ts +3 -0
  4. package/dist/binary.js +16 -0
  5. package/dist/contracts/abi.d.ts +6800 -0
  6. package/dist/contracts/abi.js +8836 -0
  7. package/dist/contracts/index.d.ts +2 -0
  8. package/dist/contracts/index.js +2 -0
  9. package/dist/contracts/utils.d.ts +25 -0
  10. package/dist/contracts/utils.js +28 -0
  11. package/dist/errors.d.ts +125 -0
  12. package/dist/errors.js +91 -0
  13. package/dist/generated/inco/shield/v2/conductor_connect.d.ts +137 -0
  14. package/dist/generated/inco/shield/v2/conductor_connect.js +141 -0
  15. package/dist/generated/inco/shield/v2/deposit_pb.d.ts +103 -0
  16. package/dist/generated/inco/shield/v2/deposit_pb.js +141 -0
  17. package/dist/generated/inco/shield/v2/drafting_pb.d.ts +279 -0
  18. package/dist/generated/inco/shield/v2/drafting_pb.js +372 -0
  19. package/dist/generated/inco/shield/v2/permission_pb.d.ts +443 -0
  20. package/dist/generated/inco/shield/v2/permission_pb.js +639 -0
  21. package/dist/generated/inco/shield/v2/query_pb.d.ts +103 -0
  22. package/dist/generated/inco/shield/v2/query_pb.js +141 -0
  23. package/dist/generated/inco/shield/v2/types_pb.d.ts +166 -0
  24. package/dist/generated/inco/shield/v2/types_pb.js +261 -0
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.js +19 -0
  27. package/dist/shield/client.d.ts +72 -0
  28. package/dist/shield/client.js +136 -0
  29. package/dist/shield/convert.d.ts +18 -0
  30. package/dist/shield/convert.js +152 -0
  31. package/dist/shield/encryption.d.ts +86 -0
  32. package/dist/shield/encryption.js +180 -0
  33. package/dist/shield/envelope.d.ts +14 -0
  34. package/dist/shield/envelope.js +29 -0
  35. package/dist/shield/index.d.ts +13 -0
  36. package/dist/shield/index.js +11 -0
  37. package/dist/shield/intent.d.ts +977 -0
  38. package/dist/shield/intent.js +188 -0
  39. package/dist/shield/rpc.d.ts +72 -0
  40. package/dist/shield/rpc.js +423 -0
  41. package/dist/shield/shield.eip712.gen.d.ts +396 -0
  42. package/dist/shield/shield.eip712.gen.js +318 -0
  43. package/dist/shield/types.d.ts +119 -0
  44. package/dist/shield/types.js +4 -0
  45. package/dist/shield/userop/gas-estimation.d.ts +36 -0
  46. package/dist/shield/userop/gas-estimation.js +284 -0
  47. package/dist/shield/userop/index.d.ts +1 -0
  48. package/dist/shield/userop/index.js +1 -0
  49. package/dist/shield/userop/token-exchange.d.ts +54 -0
  50. package/dist/shield/userop/token-exchange.js +165 -0
  51. package/dist/shield/userop/types.d.ts +91 -0
  52. package/dist/shield/userop/types.js +4 -0
  53. package/dist/shield/userop/userOp.d.ts +26 -0
  54. package/dist/shield/userop/userOp.js +98 -0
  55. package/dist/shield/utils/chain.d.ts +2 -0
  56. package/dist/shield/utils/chain.js +8 -0
  57. package/dist/uniswap-adapter/config.d.ts +34 -0
  58. package/dist/uniswap-adapter/config.js +45 -0
  59. package/dist/uniswap-adapter/find-pools.d.ts +91 -0
  60. package/dist/uniswap-adapter/find-pools.js +108 -0
  61. package/dist/uniswap-adapter/index.d.ts +10 -0
  62. package/dist/uniswap-adapter/index.js +13 -0
  63. package/dist/uniswap-adapter/swap.d.ts +344 -0
  64. package/dist/uniswap-adapter/swap.js +309 -0
  65. package/package.json +72 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Constitution: Creates an intent with gas estimation.
3
+ *
4
+ * The flow is:
5
+ * 1. Create a placeholder UserOp from the user's intent op
6
+ * 2. Get the gas estimate from the bundler using the placeholder UserOp
7
+ * 3. Assemble the final intent with the gas estimate
8
+ */
9
+ import { bytesToHex, concat, encodeAbiParameters, isAddress, isHex, maxUint64, maxUint256, zeroAddress, zeroHash, } from 'viem';
10
+ import { entryPoint07Abi, entryPoint07Address, } from 'viem/account-abstraction';
11
+ import { incoShieldAbi, shieldAbiStubAbi, teeSmartAccountAbi, } from '../../contracts/abi.js';
12
+ import { getAbiFromUnitFunction } from '../../contracts/utils.js';
13
+ import { InvalidContractReturnType } from '../../errors.js';
14
+ import { IntentKind } from '../../generated/inco/shield/v2/drafting_pb.js';
15
+ import { chequeBlobSize } from '../encryption.js';
16
+ import { assembleUserOp, buildUnsignedUserOp } from './userOp.js';
17
+ const ENTRYPOINT_VERSION = '0.7';
18
+ /** Pimlico-style dummy ECDSA sig: EIP-2 canonical `s` keeps OZ's
19
+ * `ECDSA.tryRecover` on the `ecrecover` path (else verification gas under-quotes). */
20
+ const PLACEHOLDER_ECDSA_SIG = concat([
21
+ bytesToHex(new Uint8Array(32).fill(0xff)), // r
22
+ `0x7${'a'.repeat(63)}`, // s (top nibble 0x7 → s < n/2)
23
+ '0x1c', // v
24
+ ]);
25
+ /** Placeholder `abi.encode(bytes[] signers, bytes[] sigs)`. Real signer addresses
26
+ * required: OZ's `isSigner` check runs before any `ecrecover`, so zero-address
27
+ * signers short-circuit the validation loop and under-quote gas. */
28
+ export function placeholderMultisig(signers) {
29
+ const sigs = signers.map(() => PLACEHOLDER_ECDSA_SIG);
30
+ return encodeAbiParameters([{ type: 'bytes[]' }, { type: 'bytes[]' }], [signers, sigs]);
31
+ }
32
+ /**
33
+ * Given a plaintext encoded as a 0x-prefixed hex string, return the ciphertext size.
34
+ */
35
+ function ptToCtSize(pt) {
36
+ // TODO FIXME https://github.com/Inco-fhevm/shield/issues/496
37
+ // We should use ciphertext size.
38
+ return chequeBlobSize(pt.length / 2 - 1); // Hex to bytes length
39
+ }
40
+ const TRANSFER_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'transferPlaintext')], [
41
+ {
42
+ sender: zeroAddress,
43
+ receiver: zeroAddress,
44
+ amount: maxUint256,
45
+ assetId: zeroHash,
46
+ },
47
+ ]);
48
+ const WITHDRAWAL_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'withdrawalPlaintext')], [{ sender: zeroAddress }]);
49
+ const SENDER_ESCAPE_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'senderEscapePlaintext')], [
50
+ {
51
+ sender: zeroAddress,
52
+ assetId: zeroHash,
53
+ draftingHandle: zeroHash,
54
+ spendAmount: maxUint256,
55
+ balanceSnapshot: maxUint256,
56
+ chequeNumber: maxUint64,
57
+ oldestPossiblyOutstanding: maxUint64,
58
+ },
59
+ ]);
60
+ const RECEIVER_ESCAPE_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'receiverEscapePlaintext')], [
61
+ {
62
+ receiver: zeroAddress,
63
+ assetId: zeroHash,
64
+ draftingHandle: zeroHash,
65
+ receiveAmount: maxUint256,
66
+ balanceSnapshot: maxUint256,
67
+ chequeNullifier: zeroHash,
68
+ senderChequeNumber: maxUint64,
69
+ oldestPossiblyOutstandingSend: maxUint64,
70
+ },
71
+ ]);
72
+ const INDIVIDUAL_REVOCATION_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'individualRevocationPlaintext')], [{ owner: zeroAddress, permissionId: zeroHash }]);
73
+ const USER_WIDE_REVOCATION_PLAINTEXT = encodeAbiParameters([getAbiFromUnitFunction(shieldAbiStubAbi, 'userWideRevocationPlaintext')], [{ owner: zeroAddress }]);
74
+ /** `abi.encode(address)` is one ABI word (32 bytes) regardless of adapter. */
75
+ const ERC20_ASSET_IDENTIFICATION_ARGS_SIZE = 32;
76
+ /** `abi.encode(uint256)` is one ABI word (32 bytes). */
77
+ const ERC20_MOVEMENT_ARGS_SIZE = 32;
78
+ /** Non-zero hex of `byteLength` bytes. Zero bytes cost 4 gas vs 16 for non-zero,
79
+ * so `zeroHash`-style placeholders under-quote calldata gas by ~4×. */
80
+ function nonZeroPlaceholder(byteLength) {
81
+ return bytesToHex(new Uint8Array(byteLength).fill(0xaa));
82
+ }
83
+ /** Build a placeholder {@link SettlementOp} from an `IntentOp`: intent-supplied fields
84
+ * (memo, recipient, asset movement, …) are real; enclave-derived fields use
85
+ * worst-case-sized placeholders so the estimate never under-quotes. Per-kind
86
+ * cases keep TS narrowing intact across the discriminated union.
87
+ *
88
+ * `draftingHandle` must be a handle known on-chain — `IncoShield.transfer` and
89
+ * `processWithdrawalCore` revert with `PastShieldHandleNotFound` against an
90
+ * unknown handle. Caller reads the live handle via `IncoShield.getHandle()` and
91
+ * threads it in, though it need not be the handle the enclave ultimately drafts
92
+ * against. `bytes32` slots use a nonzero placeholder so calldata gas (16/byte)
93
+ * matches the eventual real values rather than zero bytes (4/byte). */
94
+ function toPlaceholderSettlementOp(intentOp, currentHandle) {
95
+ switch (intentOp.kind) {
96
+ case IntentKind.TRANSFER:
97
+ case IntentKind.DELEGATED_TRANSFER:
98
+ return {
99
+ kind: intentOp.kind,
100
+ op: transferPlaceholder(intentOp.op.memo, currentHandle),
101
+ };
102
+ case IntentKind.WITHDRAW:
103
+ case IntentKind.DELEGATED_WITHDRAW:
104
+ return {
105
+ kind: intentOp.kind,
106
+ op: {
107
+ core: withdrawalCorePlaceholder(intentOp.op.core, currentHandle),
108
+ memo: intentOp.op.memo,
109
+ },
110
+ };
111
+ case IntentKind.DEFI_INTERACTION:
112
+ case IntentKind.DELEGATED_DEFI_INTERACTION:
113
+ return {
114
+ kind: intentOp.kind,
115
+ op: {
116
+ withdrawal: withdrawalCorePlaceholder(intentOp.op.withdrawal, currentHandle),
117
+ defiAdapter: intentOp.op.defiAdapter,
118
+ adapterArgs: intentOp.op.adapterArgs,
119
+ memo: intentOp.op.memo,
120
+ },
121
+ };
122
+ case IntentKind.INDIVIDUAL_REVOCATION:
123
+ return {
124
+ kind: intentOp.kind,
125
+ op: {
126
+ ciphertext: nonZeroPlaceholder(ptToCtSize(INDIVIDUAL_REVOCATION_PLAINTEXT)),
127
+ draftingHandle: zeroHash,
128
+ draftingTimestamp: maxUint64,
129
+ },
130
+ };
131
+ case IntentKind.USER_WIDE_REVOCATION:
132
+ return {
133
+ kind: intentOp.kind,
134
+ op: {
135
+ ciphertext: nonZeroPlaceholder(ptToCtSize(USER_WIDE_REVOCATION_PLAINTEXT)),
136
+ draftingHandle: zeroHash,
137
+ draftingTimestamp: maxUint64,
138
+ },
139
+ };
140
+ }
141
+ }
142
+ function transferPlaceholder(memo, currentHandle) {
143
+ return {
144
+ draftingHandle: currentHandle,
145
+ draftingTimestamp: maxUint64,
146
+ nullifier: nonZeroPlaceholder(32),
147
+ senderEscapeCommitment: nonZeroPlaceholder(32),
148
+ receiverEscapeCommitment: nonZeroPlaceholder(32),
149
+ senderEscapeData: nonZeroPlaceholder(ptToCtSize(SENDER_ESCAPE_PLAINTEXT)),
150
+ receiverEscapeData: nonZeroPlaceholder(ptToCtSize(RECEIVER_ESCAPE_PLAINTEXT)),
151
+ ciphertext: nonZeroPlaceholder(ptToCtSize(TRANSFER_PLAINTEXT)),
152
+ memo,
153
+ };
154
+ }
155
+ function withdrawalCorePlaceholder(core, currentHandle) {
156
+ return {
157
+ to: core.to,
158
+ nullifier: nonZeroPlaceholder(32),
159
+ amount: maxUint256,
160
+ draftingHandle: currentHandle,
161
+ draftingTimestamp: maxUint64,
162
+ assetMovement: core.assetMovement,
163
+ ciphertext: nonZeroPlaceholder(ptToCtSize(WITHDRAWAL_PLAINTEXT)),
164
+ senderEscapeCommitment: nonZeroPlaceholder(32),
165
+ senderEscapeData: nonZeroPlaceholder(ptToCtSize(SENDER_ESCAPE_PLAINTEXT)),
166
+ };
167
+ }
168
+ /** Exported for tests that pin the expected placeholder byte counts. */
169
+ export const PLACEHOLDER_SIZES = {
170
+ transferCiphertext: ptToCtSize(TRANSFER_PLAINTEXT),
171
+ withdrawalCiphertext: ptToCtSize(WITHDRAWAL_PLAINTEXT),
172
+ senderEscapeData: ptToCtSize(SENDER_ESCAPE_PLAINTEXT),
173
+ receiverEscapeData: ptToCtSize(RECEIVER_ESCAPE_PLAINTEXT),
174
+ erc20AssetIdentificationArgs: ERC20_ASSET_IDENTIFICATION_ARGS_SIZE,
175
+ erc20MovementArgs: ERC20_MOVEMENT_ARGS_SIZE,
176
+ };
177
+ /**
178
+ * Build an unsigned UserOp for bundler gas estimation.
179
+ *
180
+ * Invariant: every wire-shape field matches the size of what the conductor will
181
+ * submit. Cheque fields are sized against worst-case plaintexts (over-estimate,
182
+ * never under); adapter args are ERC-20-sized — new adapters need entries in
183
+ * `PLACEHOLDER_SIZES`.
184
+ */
185
+ export async function userOpFromIntentForGasEstimation(intentOp, signer, shieldConfig, publicClient, gas) {
186
+ const [nonce, signersForPlaceholder, currentHandle, paymasterGasLimits] = await Promise.all([
187
+ getEntryPointNonce(publicClient, signer.address),
188
+ getQuorumSignersForPlaceholder(publicClient, signer.address),
189
+ getCurrentHandle(publicClient, shieldConfig.shieldAddress),
190
+ getPaymasterGasLimits(),
191
+ ]);
192
+ const placeholderGas = {
193
+ ...gas,
194
+ callGasLimit: 0n,
195
+ verificationGasLimit: 0n,
196
+ preVerificationGas: 0n,
197
+ paymasterVerificationGasLimit: paymasterGasLimits.paymasterVerificationGasLimit,
198
+ paymasterPostOpGasLimit: paymasterGasLimits.paymasterPostOpGasLimit,
199
+ maxAmount: 0n,
200
+ };
201
+ const unsigned = buildUnsignedUserOp({
202
+ settlementOp: toPlaceholderSettlementOp(intentOp, currentHandle),
203
+ sender: signer.address,
204
+ nonce,
205
+ shieldAddress: shieldConfig.shieldAddress,
206
+ gas: placeholderGas,
207
+ });
208
+ return assembleUserOp(unsigned, placeholderMultisig(signersForPlaceholder));
209
+ /** Read the v0.7 EntryPoint sequential nonce for `sender` under default key 0. */
210
+ // TODO: implement of parrallelised (2D nonce behaviour) using some random key dont use 0 @chiranjeev13
211
+ async function getEntryPointNonce(publicClient, sender) {
212
+ const nonce = await publicClient.readContract({
213
+ address: entryPoint07Address,
214
+ abi: entryPoint07Abi,
215
+ functionName: 'getNonce',
216
+ args: [sender, 0n],
217
+ });
218
+ if (typeof nonce !== 'bigint') {
219
+ throw new InvalidContractReturnType({
220
+ message: `EntryPoint.getNonce returned non-bigint: ${typeof nonce}`,
221
+ functionName: 'EntryPoint.getNonce',
222
+ expectedType: 'bigint',
223
+ });
224
+ }
225
+ return nonce;
226
+ }
227
+ /** Read the TEE quorum signers from `TeeSmartAccount` and slice to threshold T.
228
+ * Real addresses let the bundler's `isSigner` loop traverse fully before
229
+ * short-circuiting on `DUMMY_ECDSA_SIG`. */
230
+ async function getQuorumSignersForPlaceholder(publicClient, account) {
231
+ const [signersRaw, thresholdRaw] = await Promise.all([
232
+ publicClient.readContract({
233
+ address: account,
234
+ abi: teeSmartAccountAbi,
235
+ functionName: 'getSigners',
236
+ args: [0n, maxUint64],
237
+ }),
238
+ publicClient.readContract({
239
+ address: account,
240
+ abi: teeSmartAccountAbi,
241
+ functionName: 'threshold',
242
+ }),
243
+ ]);
244
+ if (!Array.isArray(signersRaw) ||
245
+ !signersRaw.every((s) => isAddress(s))) {
246
+ throw new InvalidContractReturnType({
247
+ message: 'TeeSmartAccount.getSigners returned a non-address-array result',
248
+ functionName: 'TeeSmartAccount.getSigners',
249
+ expectedType: 'readonly Address[]',
250
+ });
251
+ }
252
+ if (typeof thresholdRaw !== 'bigint') {
253
+ throw new InvalidContractReturnType({
254
+ message: `TeeSmartAccount.threshold returned non-bigint: ${typeof thresholdRaw}`,
255
+ functionName: 'TeeSmartAccount.threshold',
256
+ expectedType: 'bigint',
257
+ });
258
+ }
259
+ return signersRaw.slice(0, Number(thresholdRaw));
260
+ }
261
+ async function getCurrentHandle(publicClient, shieldAddress) {
262
+ const handle = await publicClient.readContract({
263
+ address: shieldAddress,
264
+ abi: incoShieldAbi,
265
+ functionName: 'getHandle',
266
+ });
267
+ if (!isHex(handle) || handle.length !== 66) {
268
+ throw new InvalidContractReturnType({
269
+ message: `IncoShield.getHandle returned non-bytes32: ${String(handle)}`,
270
+ functionName: 'IncoShield.getHandle',
271
+ expectedType: 'Hex (bytes32)',
272
+ });
273
+ }
274
+ return handle;
275
+ }
276
+ async function getPaymasterGasLimits() {
277
+ const defaultPMVerificationGasLimit = 100000n;
278
+ const defaultPMPostOpGasLimit = 300000n;
279
+ return {
280
+ paymasterVerificationGasLimit: defaultPMVerificationGasLimit,
281
+ paymasterPostOpGasLimit: defaultPMPostOpGasLimit,
282
+ };
283
+ }
284
+ }
@@ -0,0 +1 @@
1
+ export { ENTRYPOINT_VERSION } from './types.js';
@@ -0,0 +1 @@
1
+ export { ENTRYPOINT_VERSION } from './types.js';
@@ -0,0 +1,54 @@
1
+ import { type Address, type Hex, type PublicClient } from 'viem';
2
+ export type AggregatorRate = {
3
+ readonly answer: bigint;
4
+ readonly decimals: number;
5
+ readonly updatedAt: bigint;
6
+ readonly description: string;
7
+ };
8
+ /**
9
+ * Fetch the latest price from a single Chainlink aggregator contract.
10
+ *
11
+ * The aggregator's `description` (e.g. "ETH / USD") encodes the pair —
12
+ * there is no on-chain way to enforce that callers passed the right address.
13
+ */
14
+ export declare function getAggregatorRate(publicClient: PublicClient, aggregator: Address): Promise<AggregatorRate>;
15
+ declare const UnknownAssetFeed_base: new <Fields extends Record<string, unknown>>(fields: Fields) => Error & {
16
+ readonly _tag: "UnknownAssetFeed";
17
+ } & Readonly<Fields>;
18
+ export declare class UnknownAssetFeed extends UnknownAssetFeed_base<{
19
+ readonly assetId: Hex;
20
+ }> {
21
+ }
22
+ /**
23
+ * Resolve the Chainlink aggregator for a Shield `assetId` (= ERC-20 token
24
+ * address padded to bytes32) and return its latest rate.
25
+ */
26
+ export declare function getAssetRate(publicClient: PublicClient, assetId: Hex): Promise<AggregatorRate>;
27
+ /** RAY = 1e27, the fixed-point scale used by the Shield paymaster. */
28
+ export declare const RAY: bigint;
29
+ /**
30
+ * Compute the RAY-precision exchange rate (ETH priced in token units)
31
+ * that the Shield paymaster consumes. Mirrors the on-chain convention
32
+ * from SHIELD_PAYMASTER.md:
33
+ *
34
+ * tokenAmount = ceil(gasCostWei * exchangeRate / 1e27)
35
+ *
36
+ * Worked example: if 1 ETH = 2000 USDC and USDC has 6 decimals,
37
+ * exchangeRate = 2000 * 1e6 * 1e27 / 1e18 = 2e18
38
+ *
39
+ * Derivation here uses two Chainlink USD feeds:
40
+ * ethInToken (real) = ethUsd / tokenUsd
41
+ * rate (RAY) = ethInToken * 10^tokenDec * 1e27 / 1e18
42
+ * = ethUsd * 10^tokenDec * 10^tokenFeedDec * RAY
43
+ * / (tokenUsd * 10^ethFeedDec * 1e18)
44
+ */
45
+ export declare function getEthExchangeRateRay(publicClient: PublicClient, assetId: Hex): Promise<bigint>;
46
+ /** `ceil(ethWei * exchangeRate / RAY)` — matches the paymaster's rounding. */
47
+ export declare function applyExchangeRateRayCeil(ethWei: bigint, exchangeRateRay: bigint): bigint;
48
+ /**
49
+ * Convenience: fetch the RAY rate for `assetId` and apply it to `ethWei`.
50
+ * Use `getEthExchangeRateRay` directly when the caller also needs to
51
+ * forward the rate to the paymaster signer.
52
+ */
53
+ export declare function convertEthWeiToTokenUnits(publicClient: PublicClient, assetId: Hex, ethWei: bigint): Promise<bigint>;
54
+ export {};
@@ -0,0 +1,165 @@
1
+ import { parseAbi } from 'viem';
2
+ import { InvalidContractReturnType, TaggedError } from '../../errors.js';
3
+ const aggregatorAbi = parseAbi([
4
+ 'function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
5
+ 'function decimals() view returns (uint8)',
6
+ 'function description() view returns (string)',
7
+ ]);
8
+ const erc20DecimalsAbi = parseAbi(['function decimals() view returns (uint8)']);
9
+ /** ETH / USD aggregator on Base Sepolia. */
10
+ const BASE_SEPOLIA_ETH_USD_FEED = '0x4aDC67696bA383F43DD60A9e78F2C97Fbbfc7cb1';
11
+ /**
12
+ * Fetch the latest price from a single Chainlink aggregator contract.
13
+ *
14
+ * The aggregator's `description` (e.g. "ETH / USD") encodes the pair —
15
+ * there is no on-chain way to enforce that callers passed the right address.
16
+ */
17
+ export async function getAggregatorRate(publicClient, aggregator) {
18
+ const [roundDataRaw, decimalsRaw, descriptionRaw] = await Promise.all([
19
+ publicClient.readContract({
20
+ address: aggregator,
21
+ abi: aggregatorAbi,
22
+ functionName: 'latestRoundData',
23
+ }),
24
+ publicClient.readContract({
25
+ address: aggregator,
26
+ abi: aggregatorAbi,
27
+ functionName: 'decimals',
28
+ }),
29
+ publicClient.readContract({
30
+ address: aggregator,
31
+ abi: aggregatorAbi,
32
+ functionName: 'description',
33
+ }),
34
+ ]);
35
+ const { answer, updatedAt } = parseLatestRoundData(roundDataRaw);
36
+ const decimals = parseDecimals(decimalsRaw);
37
+ if (typeof descriptionRaw !== 'string') {
38
+ throw new InvalidContractReturnType({
39
+ message: `Aggregator.description returned non-string: ${typeof descriptionRaw}`,
40
+ functionName: 'Aggregator.description',
41
+ expectedType: 'string',
42
+ });
43
+ }
44
+ if (answer <= 0n) {
45
+ throw new InvalidContractReturnType({
46
+ message: `Aggregator ${aggregator} returned non-positive answer (${answer})`,
47
+ functionName: 'Aggregator.latestRoundData',
48
+ expectedType: 'positive int256',
49
+ });
50
+ }
51
+ return { answer, decimals, updatedAt, description: descriptionRaw };
52
+ }
53
+ function parseLatestRoundData(value) {
54
+ if (!Array.isArray(value) ||
55
+ value.length < 5 ||
56
+ typeof value[1] !== 'bigint' ||
57
+ typeof value[3] !== 'bigint') {
58
+ throw new InvalidContractReturnType({
59
+ message: 'Aggregator.latestRoundData returned an unexpected shape',
60
+ functionName: 'Aggregator.latestRoundData',
61
+ expectedType: '[uint80, int256, uint256, uint256, uint80]',
62
+ });
63
+ }
64
+ return { answer: value[1], updatedAt: value[3] };
65
+ }
66
+ /**
67
+ * Chainlink aggregators for fee-payment assets on Base Sepolia, keyed by
68
+ * the ERC-20 token address (Shield's `assetId` is just the token address
69
+ * left-padded to bytes32). Each feed is **TOKEN / USD** — direction matters
70
+ * when converting gas cost from ETH to token units downstream.
71
+ */
72
+ const BASE_SEPOLIA_TOKEN_FEEDS = {
73
+ // Circle native USDC on Base Sepolia → USDC/USD aggregator
74
+ '0x036CbD53842c5426634e7929541eC2318f3dCF7e': '0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165',
75
+ };
76
+ export class UnknownAssetFeed extends TaggedError('UnknownAssetFeed') {
77
+ }
78
+ /**
79
+ * Resolve the Chainlink aggregator for a Shield `assetId` (= ERC-20 token
80
+ * address padded to bytes32) and return its latest rate.
81
+ */
82
+ export async function getAssetRate(publicClient, assetId) {
83
+ const tokenAddress = tokenAddressFromAssetId(assetId);
84
+ const aggregator = lookupFeed(tokenAddress);
85
+ if (aggregator === undefined) {
86
+ throw new UnknownAssetFeed({ assetId });
87
+ }
88
+ return getAggregatorRate(publicClient, aggregator);
89
+ }
90
+ /** Extract the trailing 20-byte ERC-20 address from a bytes32 assetId. */
91
+ function tokenAddressFromAssetId(assetId) {
92
+ // bytes32 hex = "0x" + 64 chars; last 40 are the address.
93
+ const hex = assetId.startsWith('0x') ? assetId.slice(2) : assetId;
94
+ return `0x${hex.slice(-40)}`;
95
+ }
96
+ /** Case-insensitive lookup against the feed registry. */
97
+ function lookupFeed(token) {
98
+ const target = token.toLowerCase();
99
+ for (const [key, aggregator] of Object.entries(BASE_SEPOLIA_TOKEN_FEEDS)) {
100
+ if (key.toLowerCase() === target)
101
+ return aggregator;
102
+ }
103
+ return undefined;
104
+ }
105
+ /** RAY = 1e27, the fixed-point scale used by the Shield paymaster. */
106
+ export const RAY = 10n ** 27n;
107
+ /**
108
+ * Compute the RAY-precision exchange rate (ETH priced in token units)
109
+ * that the Shield paymaster consumes. Mirrors the on-chain convention
110
+ * from SHIELD_PAYMASTER.md:
111
+ *
112
+ * tokenAmount = ceil(gasCostWei * exchangeRate / 1e27)
113
+ *
114
+ * Worked example: if 1 ETH = 2000 USDC and USDC has 6 decimals,
115
+ * exchangeRate = 2000 * 1e6 * 1e27 / 1e18 = 2e18
116
+ *
117
+ * Derivation here uses two Chainlink USD feeds:
118
+ * ethInToken (real) = ethUsd / tokenUsd
119
+ * rate (RAY) = ethInToken * 10^tokenDec * 1e27 / 1e18
120
+ * = ethUsd * 10^tokenDec * 10^tokenFeedDec * RAY
121
+ * / (tokenUsd * 10^ethFeedDec * 1e18)
122
+ */
123
+ export async function getEthExchangeRateRay(publicClient, assetId) {
124
+ const tokenAddress = tokenAddressFromAssetId(assetId);
125
+ const [tokenRate, ethRate, tokenDecimalsRaw] = await Promise.all([
126
+ getAssetRate(publicClient, assetId),
127
+ getAggregatorRate(publicClient, BASE_SEPOLIA_ETH_USD_FEED),
128
+ publicClient.readContract({
129
+ address: tokenAddress,
130
+ abi: erc20DecimalsAbi,
131
+ functionName: 'decimals',
132
+ }),
133
+ ]);
134
+ const tokenDecimals = parseDecimals(tokenDecimalsRaw);
135
+ const numerator = ethRate.answer *
136
+ 10n ** BigInt(tokenDecimals) *
137
+ 10n ** BigInt(tokenRate.decimals) *
138
+ RAY;
139
+ const denominator = tokenRate.answer * 10n ** BigInt(ethRate.decimals) * 10n ** 18n;
140
+ return numerator / denominator;
141
+ }
142
+ /** `ceil(ethWei * exchangeRate / RAY)` — matches the paymaster's rounding. */
143
+ export function applyExchangeRateRayCeil(ethWei, exchangeRateRay) {
144
+ const product = ethWei * exchangeRateRay;
145
+ return (product + RAY - 1n) / RAY;
146
+ }
147
+ /**
148
+ * Convenience: fetch the RAY rate for `assetId` and apply it to `ethWei`.
149
+ * Use `getEthExchangeRateRay` directly when the caller also needs to
150
+ * forward the rate to the paymaster signer.
151
+ */
152
+ export async function convertEthWeiToTokenUnits(publicClient, assetId, ethWei) {
153
+ const rate = await getEthExchangeRateRay(publicClient, assetId);
154
+ return applyExchangeRateRayCeil(ethWei, rate);
155
+ }
156
+ function parseDecimals(value) {
157
+ if (typeof value !== 'number') {
158
+ throw new InvalidContractReturnType({
159
+ message: `Aggregator.decimals returned non-number: ${typeof value}`,
160
+ functionName: 'Aggregator.decimals',
161
+ expectedType: 'uint8',
162
+ });
163
+ }
164
+ return value;
165
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Type-only surface for the UserOp builder. Runtime code lives in `userOp.ts`.
3
+ */
4
+ import type { Address } from 'viem';
5
+ import type { UserOperation } from 'viem/account-abstraction';
6
+ import type { IntentKind } from '../../generated/inco/shield/v2/drafting_pb.js';
7
+ import type * as abi from '../shield.eip712.gen.js';
8
+ import type { DefiInteraction, IndividualRevocation, Transfer, UserWideRevocation, Withdrawal } from '../shield.eip712.gen.js';
9
+ export declare const ENTRYPOINT_VERSION: "0.7";
10
+ /**
11
+ * A drafted op — the full on-chain settlement struct the enclave produces from
12
+ * an intent via `draft()`, tagged by {@link IntentKind}. This is the "cheque":
13
+ * an intent is what the user signs *before* `draft()`; the cheque is what comes
14
+ * *after*, and is exactly what `IncoShield.{transfer,withdraw,...}` settles.
15
+ *
16
+ * Delegated kinds settle through the same on-chain function — and thus the same
17
+ * struct — as their non-delegated counterparts; the delegation context rides
18
+ * inside the intent, not in a separate selector.
19
+ */
20
+ export type SettlementOp = {
21
+ kind: IntentKind.TRANSFER;
22
+ op: Transfer;
23
+ } | {
24
+ kind: IntentKind.DELEGATED_TRANSFER;
25
+ op: Transfer;
26
+ } | {
27
+ kind: IntentKind.WITHDRAW;
28
+ op: Withdrawal;
29
+ } | {
30
+ kind: IntentKind.DELEGATED_WITHDRAW;
31
+ op: Withdrawal;
32
+ } | {
33
+ kind: IntentKind.DEFI_INTERACTION;
34
+ op: DefiInteraction;
35
+ } | {
36
+ kind: IntentKind.DELEGATED_DEFI_INTERACTION;
37
+ op: DefiInteraction;
38
+ } | {
39
+ kind: IntentKind.INDIVIDUAL_REVOCATION;
40
+ op: IndividualRevocation;
41
+ } | {
42
+ kind: IntentKind.USER_WIDE_REVOCATION;
43
+ op: UserWideRevocation;
44
+ };
45
+ /**
46
+ * Domain Intent: a signed EIP-712 intent struct tagged by its {@link IntentKind}
47
+ * discriminant. This is what the user signs *before* `draft()` — the enclave
48
+ * processes an `Intent` and produces the {@link Cheque}. Each variant is the
49
+ * generated `abi.{*}Intent` struct plus a `kind` tag; delegated kinds wrap
50
+ * their non-delegated inner intent inside the generated `Delegated*Intent` shape.
51
+ */
52
+ export type Intent = TransferIntent | WithdrawalIntent | DefiInteractionIntent | IndividualRevocationIntent | UserWideRevocationIntent | DelegatedTransferIntent | DelegatedWithdrawalIntent | DelegatedDefiInteractionIntent;
53
+ export type TransferIntent = {
54
+ readonly kind: IntentKind.TRANSFER;
55
+ } & abi.TransferIntent;
56
+ export type WithdrawalIntent = {
57
+ readonly kind: IntentKind.WITHDRAW;
58
+ } & abi.WithdrawalIntent;
59
+ export type DefiInteractionIntent = {
60
+ readonly kind: IntentKind.DEFI_INTERACTION;
61
+ } & abi.DefiInteractionIntent;
62
+ export type IndividualRevocationIntent = {
63
+ readonly kind: IntentKind.INDIVIDUAL_REVOCATION;
64
+ } & abi.IndividualRevocationIntent;
65
+ export type UserWideRevocationIntent = {
66
+ readonly kind: IntentKind.USER_WIDE_REVOCATION;
67
+ } & abi.UserWideRevocationIntent;
68
+ export type DelegatedTransferIntent = {
69
+ readonly kind: IntentKind.DELEGATED_TRANSFER;
70
+ } & abi.DelegatedTransferIntent;
71
+ export type DelegatedWithdrawalIntent = {
72
+ readonly kind: IntentKind.DELEGATED_WITHDRAW;
73
+ } & abi.DelegatedWithdrawalIntent;
74
+ export type DelegatedDefiInteractionIntent = {
75
+ readonly kind: IntentKind.DELEGATED_DEFI_INTERACTION;
76
+ } & abi.DelegatedDefiInteractionIntent;
77
+ /**
78
+ * A v0.7 UserOperation before the quorum signature is attached. Quorum signing
79
+ * happens enclave-side over {@link userOpHash}; the SDK only builds and assembles.
80
+ */
81
+ export type UnsignedUserOperation = Omit<UserOperation<typeof ENTRYPOINT_VERSION>, 'signature'>;
82
+ export type BuildUnsignedUserOpParams = {
83
+ /** The drafted on-chain struct to settle, tagged by intent kind. */
84
+ settlementOp: SettlementOp;
85
+ /** AA account submitting the op — the TeeSmartAccount (sole TEE_ROLE holder). */
86
+ sender: Address;
87
+ nonce: bigint;
88
+ shieldAddress: Address;
89
+ /** Bundler-estimated limits — everything except the signed fee fields. */
90
+ gas: abi.Gas;
91
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type-only surface for the UserOp builder. Runtime code lives in `userOp.ts`.
3
+ */
4
+ export const ENTRYPOINT_VERSION = '0.7';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Constitution: builds a v0.7 UserOperation from a drafted cheque in three pure
3
+ * stages — (1) assemble the unsigned op, (2) compute the hash the quorum signs,
4
+ * (3) attach a signature. Every stage is deterministic: identical inputs produce
5
+ * identical bytes across all quorum members. That is the property the on-chain
6
+ * TEE smart account relies on when it verifies that every quorum signature is
7
+ * over the same userOpHash. Quorum signing itself is enclave-side and
8
+ * intentionally absent here — the SDK builds and assembles, it does not sign.
9
+ */
10
+ import { type Address, type Hex } from 'viem';
11
+ import { type UserOperation } from 'viem/account-abstraction';
12
+ import { type BuildUnsignedUserOpParams, ENTRYPOINT_VERSION, type SettlementOp, type UnsignedUserOperation } from './types.js';
13
+ export declare function buildUnsignedUserOp(params: BuildUnsignedUserOpParams): UnsignedUserOperation;
14
+ export declare function userOpHash(unsignedUserOp: UnsignedUserOperation, chainId: number, entryPointAddress?: Address): Hex;
15
+ export declare function assembleUserOp(unsigned: UnsignedUserOperation, signature: Hex): UserOperation<typeof ENTRYPOINT_VERSION>;
16
+ /**
17
+ * Encode the shield settlement call for a drafted {@link OnchainCheque}: the
18
+ * `kind` selects the on-chain function and `op` is its sole argument. This is
19
+ * the inner call wrapped by {@link buildUnsignedUserOp} inside `execute`, and is
20
+ * exported so the enclave can reuse the canonical encoding when it assembles
21
+ * the cheque calldata itself.
22
+ *
23
+ * TODO(#639): the enclave OnChainOp path ABI-encodes only the struct while this
24
+ * emits selector + struct. Harmonize so both produce identical calldata.
25
+ */
26
+ export declare function encodeSettlementOpCall(settlementOp: SettlementOp): Hex;