You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/smart-accounts/3-custom-instruction.mdx
+15-29Lines changed: 15 additions & 29 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,33 +34,17 @@ A destination tag forces [FAssets direct minting](/fassets/direct-minting) to cr
34
34
35
35
The custom instruction payload has two layers: the outer EIP-4337 [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) carried in the XRPL memo, and the inner [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) that the personal account runs once the controller dispatches it.
36
36
37
-
### `PackedUserOperation`
38
-
39
-
```solidity
40
-
struct PackedUserOperation {
41
-
address sender;
42
-
uint256 nonce;
43
-
bytes initCode;
44
-
bytes callData;
45
-
bytes32 accountGasLimits;
46
-
uint256 preVerificationGas;
47
-
bytes32 gasFees;
48
-
bytes paymasterAndData;
49
-
bytes signature;
50
-
}
51
-
```
52
-
53
-
Only three fields are required for Flare Smart Accounts:
37
+
Only three fields from the [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) struct are required for Flare Smart Accounts:
54
38
55
39
-`sender`**must** equal the address of the personal account derived from the XRPL sender.
56
-
Use [`getPersonalAccount(xrplOwner)`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up — the address is deterministic, so you can fetch it before the account is even deployed.
57
-
-`nonce`**must** equal the personal account's current nonce returned by [`getNonce(personalAccount)`](/smart-accounts/reference/IMasterAccountController#getnonce).
40
+
Use [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up — the address is deterministic, so you can fetch it before the account is even deployed.
41
+
-`nonce`**must** equal the personal account's current nonce returned by [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
58
42
The nonce auto-increments on every successful execution to prevent replay.
59
43
-`callData` is the calldata that the controller invokes on the personal account.
60
44
In practice, this is `abi.encodeCall(IPersonalAccount.executeUserOp, (calls))` — anything else either reverts or is rejected by the personal account's `onlyController` modifier.
61
45
62
46
The remaining fields are not validated on-chain and can be left empty.
63
-
Authorization comes from the XRPL signature on the `Payment` transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
47
+
Authorization comes from the XRPL signature on the `Payment`XRPL payment transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
64
48
If the personal account has pinned an executor via [`getExecutor`](/smart-accounts/reference/IMasterAccountController#getexecutor), only that executor is permitted to relay the mint.
65
49
66
50
### `executeUserOp` and the `Call` struct
@@ -69,22 +53,24 @@ The personal account's [`executeUserOp(Call[])`](/smart-accounts/reference/IPers
69
53
70
54
```solidity
71
55
struct Call {
72
-
address target;
73
-
uint256 value;
74
-
bytes data;
56
+
address target;
57
+
uint256 value;
58
+
bytes data;
75
59
}
76
60
77
61
function executeUserOp(Call[] calldata _calls) external payable;
78
62
```
79
63
80
64
Each call is dispatched in array order with the personal account as `msg.sender`.
81
-
If any call reverts, the whole user operation reverts with [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) — partial execution is not possible.
65
+
If any call reverts, the whole user operation reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) — partial execution is not possible.
82
66
83
-
`executeUserOp` is `payable`, so the user operation can forward FLR alongside the calls.
84
-
Funding works the usual way: send FLR to the personal account address (it is deterministic; see [State Lookup](/smart-accounts/guides/state-lookup-ts#personal-account-of-an-xrpl-address)) and the calls can spend it via `Call.value`.
67
+
`executeUserOp` is `payable`, so the user operation can forward native tokens (e.g. FLR) alongside the calls.
68
+
Funding works the usual way: send FLR to the personal account address.
85
69
86
70
### Building `callData` in TypeScript
87
71
72
+
You can build the `callData` in TypeScript using the [`encodeFunctionData`](https://viem.sh/docs/contract/encodeFunctionData#encodefunctiondata) function from `viem` library:
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` placed in the XRPL memo.
96
+
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` placed in the XRPL payment memo field.
111
97
112
98
## Replay protection
113
99
@@ -121,7 +107,7 @@ The whole pipeline is atomic with respect to the user operation:
121
107
- If `sender` does not match the personal account, the call reverts with [`InvalidSender`](/smart-accounts/reference/IMasterAccountController#invalidsender).
122
108
- If `nonce` is not the expected value, it reverts with [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
123
109
- If the memo body has the wrong length for its instruction ID, it reverts with [`InvalidMemoData`](/smart-accounts/reference/IMasterAccountController#invalidmemodata); an unrecognized instruction byte reverts with [`InvalidInstructionId`](/smart-accounts/reference/IMasterAccountController#invalidinstructionid).
124
-
- If any inner call reverts, the personal account surfaces it as [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
110
+
- If any inner call reverts, the personal account surfaces it as [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
125
111
126
112
Because the FXRP transfer is performed before the memo is decoded, **the mint succeeds even if the user operation reverts** — see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
127
-
The freshly minted FXRP lands in the personal account, and the user can recover by either re-submitting a fixed user operation (after advancing the nonce with the `0xE1` memo, which emits [`NonceIncreased`](/smart-accounts/reference/IMasterAccountController#nonceincreased)) or sweeping the FXRP via standard FAsset instructions.
113
+
The freshly minted FXRP lands in the personal account, and the user can recover by either re-submitting a fixed user operation or transferring the FXRP to another address via standard [FAssets instructions](/smart-accounts/fasset-instructions).
The [Custom Instruction overview](/smart-accounts/custom-instruction) explains how Flare smart accounts replay an [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337)`PackedUserOperation` carried in an XRPL `Payment` memo.
24
-
This guide walks through a TypeScript script that builds the user operation with Viem, sends a payment transaction with the user operation on XRPL, and waits for the [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event on Flare.
24
+
This guide walks through a TypeScript script that builds the user operation with Viem library, sends a payment transaction with the user operation on XRPL, and waits for the [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event on Flare.
25
25
26
26
The script executes three calls on three example smart contracts on the Flare blockchain.
27
27
Because the XRPL memo field is capped at roughly `1024` bytes, the calls are split into two user operations sent in sequence.
28
28
29
-
A prerequisite for the user operation to be executed is that the personal account holds enough native to cover the call values.
29
+
A prerequisite for the user operation to be executed is that the personal account holds enough native tokens to cover the call values.
30
30
The script calls forward a total of `2` C2FLR (Coston2 Flare native tokens), so fund the personal account from the [Flare faucet](https://faucet.flare.network/coston2) before running it.
31
31
The [State Lookup guide](/smart-accounts/guides/state-lookup-ts#personal-account-of-an-xrpl-address) shows how to derive the personal account address.
32
32
33
-
The full code is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter).
33
+
The full code is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter/blob/main/src/custom-instructions.ts).
The personal account address is deterministic, so we can fetch it before the account is even deployed by calling [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) on the `MasterAccountController`.
187
+
The personal account address is deterministic, so we can fetch it before the account is even deployed by calling [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) on the [`MasterAccountController`](/smart-accounts/reference/IMasterAccountController).
188
188
Before encoding the user operation, we also need the current nonce from [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
189
189
Each successful execution increments it, so passing an invalid value reverts to [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
Once the operator bridges the instruction from XRPL to Flare, the personal account runs `executeUserOp` and the `MasterAccountController` emits [`UserOperationExecuted(personalAccount, nonce)`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted).
369
-
We watch for that event with Viem's [`watchContractEvent`](https://viem.sh/docs/contract/watchContractEvent#watchcontractevent), filtering on the personal account and the nonce we submitted:
368
+
Once the operator bridges the instruction from XRPL to Flare, the personal account runs `executeUserOp` and the `MasterAccountController` emits [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) event.
369
+
We watch for that event with Viem's [`watchContractEvent`](https://viem.sh/docs/contract/watchContractEvent#watchcontractevent) function, filtering on the personal account and the nonce we submitted:
@@ -406,7 +406,7 @@ export async function waitForUserOperationExecuted({
406
406
407
407
The bridging is handled by the [Flare Data Connector](/fdc/overview), which caps the round trip at 180 seconds.
408
408
409
-
If any inner call reverts, the whole user operation reverts with [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) and the nonce does not advance.
409
+
If any inner call reverts, the whole user operation reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the nonce does not increment.
410
410
The FXRP transfer, however, is performed before the memo is decoded, so the mint succeeds even when the user operation reverts — see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
0 commit comments