Keystore Typescript SDK to interact with Axiom keystore rollup.
npm install @axiom-crypto/keystore-sdk
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,
});
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.
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
.
The SDK supports all transaction types of the keystore rollup, including Deposit
, Withdraw
, and Update
.
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"),
});
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,
});
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,
});
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 });
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 });
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 });
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,
});
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,
});
For a complete demonstration, take a look at our node.js script examples or React example.