@crossecute/sdk 0.0.0-stage → 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.
- package/LICENSE +22 -0
- package/README.md +170 -2
- package/contracts/CrossecuteScript.sol +378 -0
- package/contracts/ISafe.sol +22 -0
- package/contracts/PlanJson.sol +154 -0
- package/contracts/SafeTxHash.sol +39 -0
- package/contracts/Types.sol +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +39 -0
- package/dist/encoding.d.ts +17 -0
- package/dist/encoding.js +48 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/plan.d.ts +37 -0
- package/dist/plan.js +92 -0
- package/dist/safeTx.d.ts +86 -0
- package/dist/safeTx.js +134 -0
- package/package.json +50 -4
package/dist/safeTx.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { concat, encodeFunctionData, getAddress, hashDomain, hashStruct, hashTypedData, numberToHex, padHex } from "viem";
|
|
2
|
+
import { checkPlan } from "./plan.js";
|
|
3
|
+
export const safeTxTypes = {
|
|
4
|
+
EIP712Domain: [
|
|
5
|
+
{ name: "chainId", type: "uint256" },
|
|
6
|
+
{ name: "verifyingContract", type: "address" },
|
|
7
|
+
],
|
|
8
|
+
SafeTx: [
|
|
9
|
+
{ name: "to", type: "address" },
|
|
10
|
+
{ name: "value", type: "uint256" },
|
|
11
|
+
{ name: "data", type: "bytes" },
|
|
12
|
+
{ name: "operation", type: "uint8" },
|
|
13
|
+
{ name: "safeTxGas", type: "uint256" },
|
|
14
|
+
{ name: "baseGas", type: "uint256" },
|
|
15
|
+
{ name: "gasPrice", type: "uint256" },
|
|
16
|
+
{ name: "gasToken", type: "address" },
|
|
17
|
+
{ name: "refundReceiver", type: "address" },
|
|
18
|
+
{ name: "nonce", type: "uint256" },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* MultiSendCallOnly deployments by Safe version, from @safe-global/safe-deployments 1.37.67.
|
|
23
|
+
* `CrossecuteScript._knownMultiSendCallOnly` holds the same table; test/Vectors.t.sol writes
|
|
24
|
+
* it to vectors/ so the two are compared.
|
|
25
|
+
*/
|
|
26
|
+
export const multiSendCallOnly = {
|
|
27
|
+
"1.3.0": {
|
|
28
|
+
addresses: ["0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B"],
|
|
29
|
+
codeHash: "0xa9865ac2d9c7a1591619b188c4d88167b50df6cc0c5327fcbd1c8c75f7c066ad",
|
|
30
|
+
},
|
|
31
|
+
"1.4.1": {
|
|
32
|
+
addresses: ["0x9641d764fc13c8B624c04430C7356C1C7C8102e2"],
|
|
33
|
+
codeHash: "0xecd5bd14a08c5d2122379900b2f272bdf107a7e92423c10dd5fe3254386c9939",
|
|
34
|
+
},
|
|
35
|
+
"1.5.0": {
|
|
36
|
+
addresses: ["0xA83c336B20401Af773B6219BA5027174338D1836"],
|
|
37
|
+
codeHash: "0xcdbdcec38d2f1c7d961b0029ff8416b7e86e9974d6f0e9c9580c7d17fcfb6663",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const multiSendAbi = [
|
|
41
|
+
{ type: "function", name: "multiSend", inputs: [{ name: "transactions", type: "bytes" }], outputs: [], stateMutability: "payable" },
|
|
42
|
+
];
|
|
43
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
44
|
+
/** Parses typed data written by `CrossecuteScript._writeSafeTx`, refusing any other shape. */
|
|
45
|
+
export function parseSafeTxTypedData(json) {
|
|
46
|
+
const raw = JSON.parse(json);
|
|
47
|
+
if (JSON.stringify(raw.types) !== JSON.stringify(safeTxTypes))
|
|
48
|
+
throw new Error("types are not Safe's SafeTx types");
|
|
49
|
+
if (raw.primaryType !== "SafeTx")
|
|
50
|
+
throw new Error("primaryType is not SafeTx");
|
|
51
|
+
const domainKeys = Object.keys(raw.domain ?? {}).sort().join();
|
|
52
|
+
if (domainKeys !== "chainId,verifyingContract")
|
|
53
|
+
throw new Error("domain must hold exactly chainId and verifyingContract");
|
|
54
|
+
const m = raw.message ?? {};
|
|
55
|
+
const keys = safeTxTypes.SafeTx.map((f) => f.name);
|
|
56
|
+
if (Object.keys(m).sort().join() !== [...keys].sort().join())
|
|
57
|
+
throw new Error("message fields are not SafeTx's");
|
|
58
|
+
const uint = (v, at) => {
|
|
59
|
+
if (typeof v !== "string" || !/^[0-9]+$/.test(v))
|
|
60
|
+
throw new Error(`${at}: not a decimal string`);
|
|
61
|
+
return BigInt(v);
|
|
62
|
+
};
|
|
63
|
+
const operation = uint(m.operation, "operation");
|
|
64
|
+
if (operation > 1n)
|
|
65
|
+
throw new Error("operation is not 0 or 1");
|
|
66
|
+
if (typeof m.data !== "string" || !/^0x([0-9a-fA-F]{2})*$/.test(m.data))
|
|
67
|
+
throw new Error("data: not complete hex bytes");
|
|
68
|
+
return {
|
|
69
|
+
chainId: uint(raw.domain.chainId, "domain.chainId"),
|
|
70
|
+
safe: getAddress(raw.domain.verifyingContract),
|
|
71
|
+
tx: {
|
|
72
|
+
to: getAddress(m.to),
|
|
73
|
+
value: uint(m.value, "value"),
|
|
74
|
+
data: m.data.toLowerCase(),
|
|
75
|
+
operation: Number(operation),
|
|
76
|
+
safeTxGas: uint(m.safeTxGas, "safeTxGas"),
|
|
77
|
+
baseGas: uint(m.baseGas, "baseGas"),
|
|
78
|
+
gasPrice: uint(m.gasPrice, "gasPrice"),
|
|
79
|
+
gasToken: getAddress(m.gasToken),
|
|
80
|
+
refundReceiver: getAddress(m.refundReceiver),
|
|
81
|
+
nonce: uint(m.nonce, "nonce"),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** The domain hash, message hash and `safeTxHash` a signer's device shows. */
|
|
86
|
+
export function safeTxHashes({ chainId, safe, tx }) {
|
|
87
|
+
const domain = { chainId, verifyingContract: safe };
|
|
88
|
+
return {
|
|
89
|
+
domainHash: hashDomain({ domain, types: safeTxTypes }),
|
|
90
|
+
messageHash: hashStruct({ data: tx, primaryType: "SafeTx", types: safeTxTypes }),
|
|
91
|
+
safeTxHash: hashTypedData({ domain, types: safeTxTypes, primaryType: "SafeTx", message: tx }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** `CrossecuteScript._multiSendPacked` followed by the `multiSend` call. */
|
|
95
|
+
export function multiSendData(calls) {
|
|
96
|
+
const packed = concat(calls.flatMap((c) => [
|
|
97
|
+
"0x00",
|
|
98
|
+
c.to.toLowerCase(),
|
|
99
|
+
padHex(numberToHex(c.value), { size: 32 }),
|
|
100
|
+
padHex(numberToHex((c.data.length - 2) / 2), { size: 32 }),
|
|
101
|
+
c.data.toLowerCase(),
|
|
102
|
+
]));
|
|
103
|
+
return encodeFunctionData({ abi: multiSendAbi, functionName: "multiSend", args: [packed] });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Throws unless `envelope` is the one Safe transaction that carries `plan`: under the owner
|
|
107
|
+
* Safe's domain on the home chain, with no gas refund, and either the plan's one call directly
|
|
108
|
+
* or all of them through a known MultiSendCallOnly.
|
|
109
|
+
*/
|
|
110
|
+
export function checkSafeTx(plan, envelope) {
|
|
111
|
+
checkPlan(plan);
|
|
112
|
+
const { tx } = envelope;
|
|
113
|
+
if (envelope.chainId !== plan.homeChainId)
|
|
114
|
+
throw new Error(`domain chain ${envelope.chainId} is not the plan's ${plan.homeChainId}`);
|
|
115
|
+
if (envelope.safe !== plan.owner)
|
|
116
|
+
throw new Error(`domain Safe ${envelope.safe} is not the plan's owner ${plan.owner}`);
|
|
117
|
+
if (tx.safeTxGas !== 0n || tx.baseGas !== 0n || tx.gasPrice !== 0n || tx.gasToken !== ZERO || tx.refundReceiver !== ZERO) {
|
|
118
|
+
throw new Error("the transaction sets gas or refund fields");
|
|
119
|
+
}
|
|
120
|
+
if (plan.calls.length === 1) {
|
|
121
|
+
const [c] = plan.calls;
|
|
122
|
+
if (tx.operation !== 0 || tx.to !== c.to || tx.value !== c.value || tx.data !== c.data.toLowerCase()) {
|
|
123
|
+
throw new Error("the transaction is not the plan's call");
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const known = Object.values(multiSendCallOnly).some((d) => d.addresses.includes(tx.to));
|
|
128
|
+
if (tx.operation !== 1 || !known)
|
|
129
|
+
throw new Error("the transaction does not delegatecall a known MultiSendCallOnly");
|
|
130
|
+
if (tx.value !== 0n)
|
|
131
|
+
throw new Error("the MultiSend call carries value");
|
|
132
|
+
if (tx.data !== multiSendData(plan.calls))
|
|
133
|
+
throw new Error("the MultiSend data is not the plan's calls");
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossecute/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Foundry and TypeScript helpers for building, simulating and checking Crossecute operations",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./contracts/*": "./contracts/*"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"crossecute": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md",
|
|
20
|
+
"contracts",
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
25
|
+
"test": "npm run test:forge && npm run test:ts",
|
|
26
|
+
"test:forge": "forge test",
|
|
27
|
+
"test:ts": "vitest run",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
29
|
+
"prepack": "npm run build",
|
|
30
|
+
"test:attest": "scripts/check-forge-attest.sh"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@crossecute/protocol": "0.1.0",
|
|
34
|
+
"viem": "2.57.18"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@openzeppelin/contracts": "5.4.0",
|
|
38
|
+
"@openzeppelin/contracts-upgradeable": "5.4.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@openzeppelin/contracts": "5.4.0",
|
|
42
|
+
"@openzeppelin/contracts-upgradeable": "5.4.0",
|
|
43
|
+
"@types/node": "22.19.1",
|
|
44
|
+
"forge-std": "github:foundry-rs/forge-std#v1.17.0",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"vitest": "4.0.8"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/crossecute/sdk.git"
|
|
51
|
+
}
|
|
52
|
+
}
|