@axiom-crypto/keystore-sdk
Version:
Keystore Rollup SDK
251 lines (181 loc) • 9.37 kB
Markdown
# Keystore SDK
## Introduction
Keystore Typescript SDK to interact with Axiom keystore rollup.
## Installation
```sh
npm install @axiom-crypto/keystore-sdk
```
## Usage
### Selecting a Signature Prover Client
You can create your own authentication rule and signature prover infrastructure, or you can use one that's already built. The Keystore SDK can accept a custom authentication rule component that
conforms to the `CustomSignatureProver` interface.
To use a specific signature prover client (m-of-n ECDSA in this example), you can use:
```typescript
import { MOfNEcdsaSignatureProver, M_OF_N_ECDSA_SIG_PROVER_URL } from "@axiom-crypto/keystore-sdk";
const mOfNEcdsaClient = createSignatureProverClient({
url: M_OF_N_ECDSA_SIG_PROVER_URL,
...MOfNEcdsaSignatureProver,
});
```
#### Creating a Custom Signature Prover Client
You can create a custom signature prover client by extending `CustomSignatureProver` with 3 generic types that correspond to the fields in your custom authentication rule for `keyData`, `authData`, and `AuthInputs`. You can see the [m-of-n ECDSA Signature Prover Keystore SDK component here](./src/signature-provers/ecdsa.ts).
### Keystore Account
To initialize a new keystore account, you'll need to pass in the keccak256 hashed keyData (`dataHash`) and verifying key (`vkey`) from your desired Signature Prover. In both cases, you can also pass in an optional `NodeClient` for additional functionality.
```typescript
// Create a new NodeClient for querying the keystore rollup
const nodeClient = createNodeClient({ url: NODE_URL });
// Counterfactual account initialization
const acct = initAccountCounterfactual({ salt, dataHash, vkey, nodeClient });
```
Alterntaively, you can also initialize a keystore account with a known keystore address (32 bytes). You'll pass in the address instead of salt, along with the dataHash and vkey as before.
```typescript
// Initializing an account with a known address (you still need to pass in the `dataHash` and `vkey`)
const acct = initAccountFromAddress({ address, dataHash, vkey, nodeClient });
```
You can calculate the `dataHash` with the following signature prover client method:
```typescript
// Encoding the `keyData` for m-of-n ECDSA signature prover and then hashing it to get the `dataHash`
const keyData = mOfNEcdsaClient.keyDataEncoder({
codehash: EXAMPLE_USER_CODEHASH,
m: BigInt(1),
signersList: [account.address],
});
const dataHash = keccak256(keyData);
```
You can use `M_OF_N_ECDSA_VKEY` as the `vkey` and `SAMPLE_USER_CODEHASH` as the `codeHash`.
### Transactions
The SDK supports all transaction types of the keystore rollup, including `Deposit`, `Withdraw`, and `Update`.
#### Deposit
To create a client for the `Deposit` transaction type, you can use the `createDepositTransactionClient` function. You'll need to provide the recipient `keystoreAddress` and the deposit `amt`.
```typescript
const depositTx = await createDepositTransactionClient({
keystoreAddress: userAcct.address,
amt: parseEther("0.01"),
});
```
#### Withdraw
To create a client for the `Withdraw` transaction type, you can use the `createWithdrawTransactionClient` function. You'll need to provide the withdrawal `amt`, the recipient address `to` on L1, the `userAcct` (the keystore account initiating the withdrawal).
```typescript
const withdrawTx = await createWithdrawTransactionClient({
amt: parseEther("0.005"),
to: account.address,
userAcct,
});
```
#### Update
To create a client for the `Update` transaction type, you can use the `createUpdateTransactionClient` function. In addition to the user keystore account we created earlier, we'll be providing an optional Sponsor Account that will be sponsoring the `Update` transaction on the keystore rollup.
```typescript
const sponsorAcct = initAccountFromAddress({
address: AXIOM_SPONSOR_KEYSTORE_ADDR,
dataHash: AXIOM_SPONSOR_DATA_HASH,
vkey: mOfNEcdsaClient.vkey,
nodeClient,
});
const updateTx = await createUpdateTransactionClient({
newUserData: keyData,
newUserVkey: mOfNEcdsaClient.vkey,
userAcct,
sponsorAcct,
});
```
### Authenticating an L2 Transaction
First we obtain the transaction's signature using our account's private key:
```typescript
const txSignature = await updateTx.sign(account.privateKey);
```
Once we've signed the transaction, we'll need to use the signature prover client to generate both the user and sponsor `AuthInputs` structs that can be passed into the signature prover client's `authenticateSponsoredTransaction` function.
```typescript
// Make user and sponsor auth inputs for the m-of-n ECDSA signature prover
const userAuthInputs = mOfNEcdsaClient.makeAuthInputs({
codehash: EXAMPLE_USER_CODEHASH,
signatures: [txSignature],
signersList: [account.address],
});
const sponsorAuthInputs = mOfNEcdsaClient.makeAuthInputs({
codehash: AXIOM_SPONSOR_CODEHASH,
signatures: [],
signersList: [AXIOM_SPONSOR_EOA],
});
// Send authentication data to the signature prover
const authHash = await mOfNEcdsaClient.authenticateSponsoredTransaction({
transaction: updateTx.toBytes(),
sponsoredAuthInputs: {
userAuthInputs,
sponsorAuthInputs,
},
});
// Wait for authentication (may take several minutes)
const authenticatedTx = await mOfNEcdsaClient.waitForSponsoredAuthentication({ hash: authHash });
```
### Send an L2 Transaction
You can send an authenticated transaction to the sequencer by creating a SequencerClient and using the `sendRawTransaction` function with the authenticated transaction from the previous section. You can then call the `waitForTransactionReceipt` function to fulfill when the transaction receipt is ready.
```typescript
// Create a SequencerClient
const sequencerClient = createSequencerClient({ url: SEQUENCER_URL });
// Send the transaction to the sequencer
const txHash = await sequencerClient.sendRawTransaction({ data: authenticatedTx });
// Wait for the transaction receipt
const receipt = await sequencerClient.waitForTransactionReceipt({ hash: txHash });
```
### Send an L1-Initiated Tranaction
To perform actions such as depositing funds into your keystore account on the L2 rollup, you need to initiate a transaction from L1. This involves using an L1 `WalletClient` extended with specific L1 bridge interaction actions provided by the SDK.
```typescript
import { publicActionsL1, walletActionsL1 } from "@axiom-crypto/keystore-sdk";
import { createWalletClient, publicActions } from "viem";
const l1Client = createWalletClient({
account,
transport: http(config.l1RpcUrl),
})
.extend(publicActions)
.extend(publicActionsL1())
.extend(walletActionsL1());
```
Next, prepare the L1 transaction data using a specific transaction client (e.g., `createDepositTransactionClient`). Then, send this transaction to the L1 bridge contract using the `initiateL1Transaction` method on your extended L1 client.
Once the L1 transaction is confirmed, retrieve its receipt. From this L1 receipt, you can extract the corresponding L2 transaction hash using `getL2TransactionHashes`. Finally, use a `SequencerClient` (or `NodeClient`) to wait for the L2 transaction to be processed and get its receipt.
```typescript
// Send the deposit transaction to L1
const l1TxHash = await l1Client.initiateL1Transaction({
bridgeAddress: config.bridgeAddress,
txClient: depositTx,
});
console.log("L1 transaction hash:", l1TxHash);
// Fetch deposit transaction hash
const l1TxReceipt = await sequencerClient.waitForTransactionReceipt({ hash: l1TxHash });
const [l2TxHash] = getL2TransactionHashes(l1TxReceipt);
// Fetch deposit transaction receipt
const l2TxReceipt = await l2Client.waitForTransactionReceipt({ hash: l2TxHash });
```
### Finalize Withdrawal
To finalize a withdrawal on the keystore rollup—meaning withdrawing funds from the keystore rollup (L2) to Ethereum (L1), you first need to send a withdrawal transaction on L2 and wait for it to be finalized. After that, you build the finalization arguments and call the `finalizeWithdrawal` method on the L1 client.
To finalize a withdrawal transaction:
```typescript
await l2Client.waitForTransactionFinalization({ hash: withdrawTxHash });
const finalizationArgs = await l2Client.buildFinalizeWithdrawalArgs({
transactionHash: withdrawTxHash,
});
const l1TxHash = await l1Client.finalizeWithdrawal({
bridgeAddress,
...finalizationArgs,
});
```
### Query the Chain
You can query the keystore rollup chain using the `KeystoreNodeProvider`. This provider enables you to retrieve various pieces of on-chain data, such as transaction details, receipts, blocks, and rollup state:
```typescript
const nodeClient = createNodeClient({ url: NODE_URL });
// get transaction by hash
const tx = await nodeClient.getTransactionByHash({ hash });
// get transaction receipt by hash
const receipt = await nodeClient.getTransactionReceipt({ hash });
// get the latest block with full transactions
const block = await nodeClient.getBlockByNumber({
block: BlockTag.Latest,
txKind: BlockTransactionsKind.Full,
});
// get account state
const accountState = await nodeClient.getStateAt({
address: keystoreAddress,
block: BlockTag.Latest,
});
```
## Examples
For a complete demonstration, take a look at our [node.js script examples](./examples/) or [React example](https://github.com/axiom-crypto/example-keystore-web).