Skip to content

Commit 45cef99

Browse files
authored
Merge pull request #20 from trustwallet/feat/add-simulator
Feat/add simulator
2 parents 404c683 + d30f48b commit 45cef99

File tree

20 files changed

+805
-346
lines changed

20 files changed

+805
-346
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ after installing the sdk, import the visualize function
2727
``` typescript
2828
import { visualize } from '@trustwallet/wizard-sdk';
2929
```
30-
The visualize is an async function that accepts two parameters, the `message` as an arbitrary object and `domain` as a strongly typed object aligned with EIP-712 domain structure and returns `Result` promise or can **throw** if the protocol is not handled or wrong message or domain content for example.
30+
The visualize is an async function that accepts two parameters, the `message` as an arbitrary object and `domain` as a strongly typed object aligned with EIP-712 domain structure and returns `VisualizationResult` promise or can **throw** if the protocol is not handled or wrong message or domain content for example.
3131

3232
```typescript
3333
/**
3434
* @param {T} message EIP-712 message
3535
* @param {Domain} domain EIP-712 domain
36-
* @returns {Result} assets impact and message liveness
36+
* @returns {VisualizationResult} assets impact and message liveness
3737
* @throws {Error}
3838
*/
3939
async function visualize<T extends object>(
4040
message: T,
4141
domain: Domain
42-
): Promise<Result>;
42+
): Promise<VisualizationResult>;
4343

4444
type Domain = {
4545
verifyingContract: string;
@@ -49,9 +49,9 @@ type Domain = {
4949
};
5050
```
5151

52-
The returned `Result` will contain some important fields like:
53-
- `assetIn`, an array of of assets that user should receive.
54-
- `assetOut`, an array of the assets that are leaving user wallet.
52+
The returned `VisualizationResult` will contain some important fields like:
53+
- `assetsIn`, an array of of assets that user should receive.
54+
- `assetsOut`, an array of the assets that are leaving user wallet.
5555
- `approval`, an array of assets that user is granting an `operator` approval for.
5656
- `liviness`, an optional object that highlight from and till which timestamp the above information is considered valid by the protocol
5757

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@trustwallet/wizard-sdk",
3-
"version": "0.1.1-beta",
3+
"version": "0.2.0-beta",
44
"description": "typescript sdk to visualize EIP-712 various protocols, simulate and decode transactions",
55
"main": "lib/index.js",
66
"repository": "git@github.com:trustwallet/wizard-sdk.git",
@@ -22,12 +22,13 @@
2222
"jest": "^29.5.0",
2323
"prettier": "^2.8.4",
2424
"ts-jest": "^29.0.5",
25+
"eslint": "^8.36.0",
2526
"typescript": "^5.0.2"
2627
},
2728
"dependencies": {
2829
"@typescript-eslint/eslint-plugin": "^5.55.0",
2930
"@typescript-eslint/parser": "^5.55.0",
30-
"eslint": "^8.36.0",
31+
"axios": "^1.3.5",
3132
"ethers": "^6.1.0"
3233
},
3334
"files": [

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import visualizer from "./visualizer";
2+
import Simulator from "./simulator";
23

3-
export { visualizer };
4+
export { visualizer, Simulator };

src/simulator/index.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { ASSET_TYPE } from "../types";
2+
import {
3+
Approval,
4+
AssetsTransfersAndApprovalsRes,
5+
DebugCallTracerWithLogs,
6+
DebugProvider,
7+
SimulationParams,
8+
Transfer,
9+
} from "../types/simulator";
10+
import {
11+
ERC1155_TRANSFER_BATCH_TOPIC,
12+
ERC1155_TRANSFER_SINGLE_TOPIC,
13+
ERC20_ERC721_APPROVE_TOPIC,
14+
ERC20_ERC721_TRANSFER_TOPIC,
15+
ERC721_ERC1155_APPROVE_ALL_TOPIC,
16+
TRUE,
17+
ZERO_ADDRESS,
18+
abiCoder,
19+
addressFrom32bytesTo20bytes,
20+
decodeErrorMessage,
21+
} from "../utils";
22+
import { nodeRealFactory } from "./providers";
23+
24+
const { ERC1155, ERC20, ERC721, NATIVE } = ASSET_TYPE;
25+
export default class Simulator {
26+
private providers: {
27+
[chainId: number]: DebugProvider["debugTrace"];
28+
} = {};
29+
30+
constructor(providerInstances: DebugProvider[]) {
31+
providerInstances.forEach((p) => {
32+
this.providers[p.chainId] = p.debugTrace;
33+
});
34+
}
35+
36+
async simulate(params: SimulationParams): Promise<AssetsTransfersAndApprovalsRes> {
37+
if (!params.chainId) throw new Error("undefined chainId param");
38+
39+
if (!params.from) throw new Error("undefined from param");
40+
41+
if (!params.to) throw new Error("undefined to param");
42+
43+
if (!params.calldata) throw new Error("undefined to calldata");
44+
45+
const debugTraceCall = this.providers[params.chainId];
46+
47+
if (!debugTraceCall) {
48+
throw new Error(`Simulator: no debugProvider found for chainId: ${params.chainId}`);
49+
}
50+
51+
const trace = await debugTraceCall(params);
52+
const transfers: Transfer[] = [];
53+
const approvals: Approval[] = [];
54+
55+
this.processTraceCallData(trace, transfers, approvals);
56+
return {
57+
transfers,
58+
approvals,
59+
};
60+
}
61+
62+
/**
63+
* @dev recursive function to precess top and internal calls
64+
* and mutably append records to both `transfers` and `approvals` arrays
65+
* the function classify and decode data according to EVM specification and Ethereum EIPs
66+
* @see https://eips.ethereum.org/EIPS/eip-721
67+
* @see https://eips.ethereum.org/EIPS/eip-20
68+
* @see https://eips.ethereum.org/EIPS/eip-1155
69+
* @see https://docs.soliditylang.org/en/v0.8.13/abi-spec.html
70+
* @param {DebugCallTracerWithLogs} data data to be processed recursively
71+
* @param {Transfer[]} transfersReference mutable array to append transfer records
72+
* @param {Approval[]} approvalsReference mutable array to append approval records
73+
*/
74+
private processTraceCallData = (
75+
data: DebugCallTracerWithLogs,
76+
transfersReference: Transfer[],
77+
approvalsReference: Approval[]
78+
) => {
79+
data?.forEach((call) => {
80+
if (call.error) {
81+
throw new Error(
82+
`Transaction will fail with error: '${decodeErrorMessage(call.output)}'`
83+
);
84+
}
85+
// handle native ETH transfers
86+
if (call.value && call.value !== "0x0") {
87+
transfersReference.push({
88+
address: ZERO_ADDRESS,
89+
type: NATIVE,
90+
from: call.from,
91+
to: call.to,
92+
amount: call.value,
93+
id: "",
94+
});
95+
}
96+
97+
// handle logs
98+
call.logs?.forEach((event) => {
99+
const eventHash = event.topics[0];
100+
101+
// ERC20 and ERC721 Transfer events
102+
// keccak256(Transfer(address,address,uint256))
103+
if (eventHash === ERC20_ERC721_TRANSFER_TOPIC) {
104+
// ERC20 Transfer event have a topics array of 3 elements (eventHash, and 2 indexed addresses)
105+
if (event.topics.length === 3) {
106+
const [, _from, _to] = event.topics;
107+
const from = addressFrom32bytesTo20bytes(_from);
108+
const to = addressFrom32bytesTo20bytes(_to);
109+
const amount = BigInt(event.data).toString();
110+
transfersReference.push({
111+
address: event.address,
112+
type: ERC20,
113+
from,
114+
to,
115+
amount,
116+
});
117+
}
118+
// ERC721 Transfer event have a topics array of 4 elements (eventHash, and 2 indexed addresses and an indexed tokenId)
119+
else {
120+
const [, _from, _to, _tokenId] = event.topics;
121+
const from = addressFrom32bytesTo20bytes(_from);
122+
const to = addressFrom32bytesTo20bytes(_to);
123+
const tokenId = BigInt(_tokenId).toString();
124+
transfersReference.push({
125+
address: event.address,
126+
type: ERC721,
127+
from,
128+
to,
129+
id: tokenId,
130+
});
131+
}
132+
}
133+
// ERC20 and ERC721 Approval events
134+
// keccak256(Approval(address,address,uint256))
135+
else if (eventHash === ERC20_ERC721_APPROVE_TOPIC) {
136+
// ERC20 Approval event have a topics array of 3 elements (eventHash, and 2 indexed addresses)
137+
if (event.topics.length === 3) {
138+
const [, _owner, _operator] = event.topics;
139+
const owner = addressFrom32bytesTo20bytes(_owner);
140+
const operator = addressFrom32bytesTo20bytes(_operator);
141+
142+
const amount = BigInt(event.data).toString();
143+
approvalsReference.push({
144+
address: event.address,
145+
type: ERC20,
146+
owner,
147+
operator,
148+
amount,
149+
});
150+
}
151+
// ERC721 Approval event have a topics array of 4 elements (eventHash, and 2 indexed addresses and an indexed tokenId)
152+
else if (event.topics.length === 4) {
153+
const [, _owner, _operator, _tokenId] = event.topics;
154+
const owner = addressFrom32bytesTo20bytes(_owner);
155+
const operator = addressFrom32bytesTo20bytes(_operator);
156+
const tokenId = BigInt(_tokenId).toString();
157+
approvalsReference.push({
158+
address: event.address,
159+
type: ERC721,
160+
owner,
161+
operator,
162+
id: tokenId,
163+
});
164+
}
165+
}
166+
// ERC721 and ERC1155 ApprovalForAll event
167+
// keccak256(ApprovalForAll(address,address,bool))
168+
else if (eventHash === ERC721_ERC1155_APPROVE_ALL_TOPIC) {
169+
const [, _owner, _operator] = event.topics;
170+
const owner = addressFrom32bytesTo20bytes(_owner);
171+
const operator = addressFrom32bytesTo20bytes(_operator);
172+
const isApprove = event.data === TRUE;
173+
approvalsReference.push({
174+
address: event.address,
175+
type: ERC721,
176+
owner,
177+
operator,
178+
approveForAllStatus: isApprove,
179+
});
180+
}
181+
182+
// ERC1155 TransferSingle event
183+
// keccak256(TransferSingle(address,address,address,uint256,uint256))
184+
else if (eventHash === ERC1155_TRANSFER_SINGLE_TOPIC) {
185+
const [, _operator, _from, _to] = event.topics;
186+
const operator = addressFrom32bytesTo20bytes(_operator);
187+
const from = addressFrom32bytesTo20bytes(_from);
188+
const to = addressFrom32bytesTo20bytes(_to);
189+
const [id, amount] = abiCoder.decode(["uint256", "uint256"], event.data);
190+
transfersReference.push({
191+
address: event.address,
192+
type: ERC1155,
193+
from,
194+
to,
195+
operator,
196+
amount: BigInt(amount).toString(),
197+
id: BigInt(id).toString(),
198+
});
199+
}
200+
201+
// ERC1155 TransferBatch event
202+
// keccak256(TransferBatch(address,address,address,uint256[],uint256[]))
203+
else if (eventHash === ERC1155_TRANSFER_BATCH_TOPIC) {
204+
const [, _operator, _from, _to] = event.topics;
205+
const operator = addressFrom32bytesTo20bytes(_operator);
206+
const from = addressFrom32bytesTo20bytes(_from);
207+
const to = addressFrom32bytesTo20bytes(_to);
208+
const [ids, amounts] = abiCoder.decode(["uint256[]", "uint256[]"], event.data);
209+
210+
(ids as bigint[]).forEach((id, index) => {
211+
const amount = amounts[index];
212+
transfersReference.push({
213+
address: event.address,
214+
type: ERC1155,
215+
from,
216+
to,
217+
operator,
218+
amount: BigInt(amount).toString(),
219+
id: BigInt(id).toString(),
220+
});
221+
});
222+
}
223+
});
224+
225+
// recursive call to process internal calls, since the EVM use gas and the call stack deterministic there's no risk of infinite looping
226+
this.processTraceCallData(call.calls, transfersReference, approvalsReference);
227+
});
228+
};
229+
230+
static nodeRealFactory = nodeRealFactory;
231+
}

src/simulator/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { nodeRealFactory } from "./nodeReal";

0 commit comments

Comments
 (0)