|
2 | 2 | sidebar_position: 4 |
3 | 3 | --- |
4 | 4 |
|
5 | | -# Account Abstraction |
| 5 | +# Account Abstraction & Session Keys in TEN |
6 | 6 |
|
7 | | -The key feature of [Account Abstraction](https://medium.com/p/2e85bde4c54d) (EIP-4337) is “session keys” (SKs) through a proxy smart contract. |
8 | | -SKs allow users to interact with the blockchain without signing every transaction, which is a major UX improvement. |
| 7 | +In the classic Account Abstraction model (EIP‑4337) described on [ethereum.org](https://ethereum.org/roadmap/account-abstraction/), “session keys” (SKs) are typically implemented using proxy smart contracts and a bundler. |
| 8 | +TEN’s Account Abstraction design, outlined in [this post](https://medium.com/p/2e85bde4c54d), instead provides **native session keys managed by the gateway and TEEs**, eliminating the need for proxy contracts or a bundler. |
9 | 9 |
|
10 | | -TEN supports "native" SKs - these are managed by the platform and do not require a proxy contract. |
| 10 | +Session keys in TEN are: |
11 | 11 |
|
12 | | -In TEN, SKs are managed by dApp developers through dedicated RPC endpoints. |
| 12 | +- **Ephemeral Ethereum accounts** managed by the gateway on behalf of a user. |
| 13 | +- **Linked to the user’s viewing key / encryption token** ([details](https://docs.ten.xyz/docs/write-ten-dapp/programmable-gateway-access)). |
| 14 | +- **Used to sign and submit transactions** via the gateway, enabling “no‑click” UX for dApps. |
13 | 15 |
|
14 | | -## Solution overview |
| 16 | +Each user can have **multiple session keys** (up to 100), and the gateway can automatically expire idle keys and recover funds back to the user’s primary account. |
15 | 17 |
|
16 | | -Imagine you're developing an on-chain game, and you want a smooth UX without the distraction of signing every move. |
| 18 | +--- |
| 19 | + |
| 20 | +## High‑level flow for dApps |
17 | 21 |
|
18 | | -Conceptually, the game will create a session key (SK) for the user, then ask the user to move some funds to that address, and then create “move” transactions signed with the SK. |
| 22 | +Imagine an on‑chain game that wants a smooth UX without prompting the user to sign every move: |
19 | 23 |
|
20 | | -If the game were to create the SK in the browser, there would be a risk of the user losing the SK, and the funds associated with it, in case of an accidental exit. |
21 | | -With TEN, the dApp developer doesn't have to worry about this, because the SKs are managed by TEEs. |
| 24 | +1. **User authenticates to the gateway** and gets an `encryptionToken`. |
| 25 | +2. **Gateway creates a session key** for this user and exposes its address. |
| 26 | +3. **User funds the session key** with a normal, user‑signed transfer. |
| 27 | +4. **Game sends transactions using the session key** (no additional wallet popups). |
| 28 | +5. **Gateway tracks activity and can expire/clean up idle session keys**, sweeping funds back to the account which was first authenticated with the gateway. |
22 | 29 |
|
23 | | -## Usage |
| 30 | +From the dApp’s point of view, a session key behaves like a normal Ethereum account whose private key is held inside the TEE‑backed gateway. |
24 | 31 |
|
25 | | -The steps below describe the implementation for a game developer—the primary use case for SKs. |
26 | | -Note that SKs can be used for any dApp that requires a no‑click UX. |
| 32 | +--- |
27 | 33 |
|
28 | | -### When the game starts |
| 34 | +## Using session keys via custom queries |
29 | 35 |
|
30 | | -Before the user can start playing, the game must create the SK and ask the user to move some funds to that address. |
31 | | -The funds will be used to pay for moves. |
| 36 | +On TEN, session key management is exposed as **custom queries** behind standard JSON‑RPC, primarily via `eth_getStorageAt` with special “method addresses”. |
32 | 37 |
|
33 | | -- Call the RPC `eth_getStorageAt` with address `0x0000000000000000000000000000000000000003` - this will return the hex-encoded address of the SK. The dApp needs to store this address for future use. |
34 | | -- Create a normal transaction that transfers some ETH to the SK. The amount depends on how many "moves" the user is prepared to prepay for. |
35 | | -- Ask the user to sign this transaction with their standard wallet, and submit it to the network using the library of your choice. |
36 | | -- The session key is automatically activated and ready to use. |
| 38 | +All examples below assume you are calling through the gateway: |
37 | 39 |
|
38 | | -### During the game |
| 40 | +```text |
| 41 | +https://testnet-rpc.ten.xyz/v1/?token=<EncryptionToken> |
| 42 | +``` |
39 | 43 |
|
40 | | -After sending funds to the SK, create a transaction for each move, but don't ask the user to sign them. |
41 | | -Instead, submit them to the network unsigned using the RPC `eth_getStorageAt` with address `0x0000000000000000000000000000000000000005` and the following parameters: |
| 44 | +### 1. Create a session key |
42 | 45 |
|
43 | | -```json |
44 | | -{ |
45 | | - "sessionKeyAddress": "0x...", // The session key address |
46 | | - "tx": "base64_encoded_transaction" // The unsigned transaction encoded as base64 |
| 46 | +To create a new session key for the authenticated user: |
| 47 | + |
| 48 | +- Call `eth_getStorageAt` with: |
| 49 | + - **address**: `0x0000000000000000000000000000000000000003` |
| 50 | + - other parameters can be left as in the example below. |
| 51 | + |
| 52 | +The response returns the **hex‑encoded address** of the new session key. |
| 53 | + |
| 54 | +```javascript |
| 55 | +async function createSessionKey(encryptionToken) { |
| 56 | + const response = await fetch( |
| 57 | + `https://testnet-rpc.ten.xyz/v1/?token=${encryptionToken}`, |
| 58 | + { |
| 59 | + method: "POST", |
| 60 | + headers: { "Content-Type": "application/json" }, |
| 61 | + body: JSON.stringify({ |
| 62 | + jsonrpc: "2.0", |
| 63 | + method: "eth_getStorageAt", |
| 64 | + params: [ |
| 65 | + "0x0000000000000000000000000000000000000003", // create SK |
| 66 | + "0x0", |
| 67 | + "latest", |
| 68 | + ], |
| 69 | + id: 1, |
| 70 | + }), |
| 71 | + }, |
| 72 | + ); |
| 73 | + |
| 74 | + const data = await response.json(); |
| 75 | + return data.result; // Session key address (0x...) |
47 | 76 | } |
48 | 77 | ``` |
49 | 78 |
|
50 | | -The platform will sign the transactions on behalf of the user. |
| 79 | +### 2. Fund the session key |
51 | 80 |
|
52 | | -As a game developer, you are responsible for keeping track of the SK’s balance. You can also query the network for the balance of the SK address. |
53 | | -If the SK runs out of balance, you must ask the user to move more funds to the SK. |
| 81 | +Once you have the session key address, fund it with a **normal user‑signed transaction** from the user’s main wallet: |
54 | 82 |
|
55 | | -### Managing session keys |
| 83 | +- Build a standard `eth_sendTransaction` / wallet transaction from the user’s account to the session key address. |
| 84 | +- Let the wallet sign and submit it. |
56 | 85 |
|
57 | | -TEN provides additional RPC endpoints for managing session keys: |
| 86 | +The gateway observes this and considers the session key funded and ready to use. |
58 | 87 |
|
59 | | -- `eth_getStorageAt` with address `0x0000000000000000000000000000000000000004` — permanently removes the session key. This requires the following parameters: |
| 88 | +--- |
| 89 | + |
| 90 | +## Sending transactions with a session key |
| 91 | + |
| 92 | +There are two main integration patterns: |
| 93 | + |
| 94 | +1. **Custom‑query based signing** (low‑level; uses `eth_getStorageAt`). |
| 95 | +2. **Direct `eth_sendTransaction` from the session key** (simpler, when your client library supports it). |
| 96 | + |
| 97 | +### 1) Custom‑query signing (low‑level) |
| 98 | + |
| 99 | +You can ask the gateway to sign and submit an unsigned transaction using the session key by calling: |
| 100 | + |
| 101 | +- `eth_getStorageAt` with **address** `0x0000000000000000000000000000000000000005` |
| 102 | +- The second parameter encodes: |
60 | 103 |
|
61 | 104 | ```json |
62 | 105 | { |
63 | | - "sessionKeyAddress": "0x..." // The session key address to delete |
| 106 | + "sessionKeyAddress": "0x...", // Session key address |
| 107 | + "tx": "base64_encoded_transaction" // Unsigned transaction, base64‑encoded |
64 | 108 | } |
65 | 109 | ``` |
66 | 110 |
|
67 | | -### Finishing the game |
| 111 | +Example: |
68 | 112 |
|
69 | | -When a game ends, you must move the remaining funds back to the main address. |
| 113 | +```javascript |
| 114 | +async function sendWithSessionKey( |
| 115 | + encryptionToken, |
| 116 | + sessionKeyAddress, |
| 117 | + unsignedTx, |
| 118 | +) { |
| 119 | + const txBase64 = btoa(JSON.stringify(unsignedTx)); // client‑side encoding |
| 120 | + |
| 121 | + const response = await fetch( |
| 122 | + `https://testnet-rpc.ten.xyz/v1/?token=${encryptionToken}`, |
| 123 | + { |
| 124 | + method: "POST", |
| 125 | + headers: { "Content-Type": "application/json" }, |
| 126 | + body: JSON.stringify({ |
| 127 | + jsonrpc: "2.0", |
| 128 | + method: "eth_getStorageAt", |
| 129 | + params: [ |
| 130 | + "0x0000000000000000000000000000000000000005", // sign+send with SK |
| 131 | + JSON.stringify({ |
| 132 | + sessionKeyAddress, |
| 133 | + tx: txBase64, |
| 134 | + }), |
| 135 | + "latest", |
| 136 | + ], |
| 137 | + id: 1, |
| 138 | + }), |
| 139 | + }, |
| 140 | + ); |
70 | 141 |
|
71 | | -- Create a transaction (tx) that moves the funds back from the SK to the main address. Submit it unsigned, because the funds are controlled by the SK. |
| 142 | + const data = await response.json(); |
| 143 | + return data.result; // tx hash |
| 144 | +} |
| 145 | +``` |
72 | 146 |
|
73 | | -## Example implementation |
| 147 | +The gateway first confirms that the session key belongs to the user identified by `encryptionToken`. It then signs the transaction with the session key’s private key and sends it to the TEN node. |
74 | 148 |
|
75 | | -Here's a complete example of how to implement session keys in a JavaScript dApp: |
| 149 | +### 2) Direct `eth_sendTransaction` with a session key |
76 | 150 |
|
77 | | -```javascript |
78 | | -// 1. Create a session key |
79 | | -async function createSessionKey() { |
80 | | - const response = await fetch("https://testnet.ten.xyz/v1/", { |
81 | | - method: "POST", |
82 | | - headers: { "Content-Type": "application/json" }, |
83 | | - body: JSON.stringify({ |
84 | | - jsonrpc: "2.0", |
85 | | - method: "eth_getStorageAt", |
86 | | - params: ["0x0000000000000000000000000000000000000003", "0x0", "latest"], |
87 | | - id: 1, |
88 | | - }), |
89 | | - }); |
| 151 | +You can send transactions directly using `eth_sendTransaction` by setting the `from` field to a session key address that belongs to the authenticated user. The gateway signs the transaction with the session key's private key and broadcasts it. |
90 | 152 |
|
91 | | - const data = await response.json(); |
92 | | - return data.result; // Returns the session key address |
93 | | -} |
| 153 | +**Requirements:** |
94 | 154 |
|
95 | | -// 2. Fund the session key (user signs this transaction) |
96 | | -async function fundSessionKey(sessionKeyAddress, amount) { |
97 | | - // This would be a normal transaction signed by the user's wallet |
98 | | - // transferring ETH to the session key address |
99 | | -} |
| 155 | +- The request must include the user's `encryptionToken` (same authentication as other gateway calls). |
| 156 | +- The `from` address must be a session key owned by that user. |
| 157 | +- The transaction must include all required fields (`gas`, `gasPrice` or `maxFeePerGas`/`maxPriorityFeePerGas`, `nonce`, etc.). The gateway does not auto-fill missing fields. |
100 | 158 |
|
101 | | -// 3. Send unsigned transactions using the session key |
102 | | -async function sendUnsignedTransaction(sessionKeyAddress, unsignedTx) { |
103 | | - const txBase64 = btoa(JSON.stringify(unsignedTx)); // Convert to base64 |
104 | | - |
105 | | - const response = await fetch("https://testnet.ten.xyz/v1/", { |
106 | | - method: "POST", |
107 | | - headers: { "Content-Type": "application/json" }, |
108 | | - body: JSON.stringify({ |
109 | | - jsonrpc: "2.0", |
110 | | - method: "eth_getStorageAt", |
111 | | - params: [ |
112 | | - "0x0000000000000000000000000000000000000005", |
113 | | - JSON.stringify({ |
114 | | - sessionKeyAddress: sessionKeyAddress, |
115 | | - tx: txBase64, |
116 | | - }), |
117 | | - "latest", |
118 | | - ], |
119 | | - id: 1, |
120 | | - }), |
121 | | - }); |
| 159 | +The gateway verifies that the session key belongs to the user identified by `encryptionToken`, signs the transaction with the session key's private key, and sends it to the TEN node. |
122 | 160 |
|
123 | | - const data = await response.json(); |
124 | | - return data.result; // Returns the transaction hash |
125 | | -} |
| 161 | +**Error handling:** |
126 | 162 |
|
127 | | -// 4. Delete the session key when done |
128 | | -async function deleteSessionKey(sessionKeyAddress) { |
129 | | - const response = await fetch("https://testnet.ten.xyz/v1/", { |
130 | | - method: "POST", |
131 | | - headers: { "Content-Type": "application/json" }, |
132 | | - body: JSON.stringify({ |
133 | | - jsonrpc: "2.0", |
134 | | - method: "eth_getStorageAt", |
135 | | - params: [ |
136 | | - "0x0000000000000000000000000000000000000004", |
137 | | - JSON.stringify({ |
138 | | - sessionKeyAddress: sessionKeyAddress, |
139 | | - }), |
140 | | - "latest", |
141 | | - ], |
142 | | - id: 1, |
143 | | - }), |
144 | | - }); |
| 163 | +- If `from` is not a session key for the authenticated user, the call fails with an error indicating the session key address was not found. |
| 164 | +- If the session key lacks sufficient balance, the transaction is rejected by the network (not by the gateway). |
| 165 | +- Standard authentication errors apply if the `encryptionToken` is missing or invalid. |
145 | 166 |
|
146 | | - const data = await response.json(); |
147 | | - return data.result; // Returns 0x01 for success, 0x00 for failure |
| 167 | +--- |
| 168 | + |
| 169 | +## Deleting and expiring session keys |
| 170 | + |
| 171 | +### Manual deletion via custom query |
| 172 | + |
| 173 | +To explicitly delete a session key: |
| 174 | + |
| 175 | +- Call `eth_getStorageAt` with **address** `0x0000000000000000000000000000000000000004` and a JSON payload: |
| 176 | + |
| 177 | +```json |
| 178 | +{ |
| 179 | + "sessionKeyAddress": "0x..." // Session key address to delete |
148 | 180 | } |
149 | 181 | ``` |
| 182 | + |
| 183 | +The gateway: |
| 184 | + |
| 185 | +- Removes the session key from storage (access to the private key is lost forever). |
| 186 | +- **Sweeps remaining funds** back to the user’s primary account, using its internal `TxSender`. |
| 187 | + |
| 188 | +### Automatic expiration & fund recovery |
| 189 | + |
| 190 | +In addition, the gateway runs a background **SessionKeyExpirationService**: |
| 191 | + |
| 192 | +- Tracks last activity for each session key. |
| 193 | +- Periodically scans for keys older than `--sessionKeyExpirationThreshold`. |
| 194 | +- For each candidate, it: |
| 195 | + - Reloads the owning user. |
| 196 | + - Attempts to move remaining funds back to the user’s primary account. |
| 197 | + - Removes the key from the activity tracker and persists updated state. |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## UX & safety considerations for dApp developers |
| 202 | + |
| 203 | +- **Balance management**: Track the session key’s balance (e.g. via `eth_getBalance`) and top it up when needed. |
| 204 | +- **Per‑dApp keys**: Use separate session keys per dApp / game instance if you want stronger isolation. |
| 205 | +- **Lifecycle**: Delete or let the gateway expire session keys when they are no longer needed to keep the system tidy. |
| 206 | +- **Security**: The session key’s private key never leaves the TEE‑backed gateway, but users should still treat them as scoped spending keys and only fund them with limited amounts. |
| 207 | + |
| 208 | +Combined with **programmable access tokens** (`encryptionToken`) and the TEN gateway, session keys give you EIP‑4337‑style UX without additional contracts or bundler infrastructure. |
0 commit comments