Skip to content

axiom-crypto/axiom-keystore-sdk

Repository files navigation

Keystore SDK

Introduction

Keystore Typescript SDK to interact with Axiom keystore rollup.

Installation

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:

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.

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.

// 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.

// 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:

// 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.

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).

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.

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:

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.

// 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.

// 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.

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.

// 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:

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:

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 or React example.

About

SDK for the Axiom Keystore Rollup

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 6