Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core extension for viem #2202

Merged
merged 15 commits into from
Mar 20, 2025
11 changes: 7 additions & 4 deletions app/(home)/tools/l1-toolbox/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use client'
import dynamic from 'next/dynamic';
import { default as L1Toolbox } from "@/toolbox/src/demo/ToolboxApp"

export default function L1LauncherPage() {
const NoSSRL1Toolbox = dynamic(() => Promise.resolve(L1Toolbox), { ssr: false });
// Import the component lazily with no SSR
const L1Toolbox = dynamic(() => import("@/toolbox/src/demo/ToolboxApp"), {
ssr: false,
loading: () => <div>Loading...</div>
});

export default function L1LauncherPage() {
return (
<div className="">
<NoSSRL1Toolbox />
<L1Toolbox />
</div>
);
}
37 changes: 37 additions & 0 deletions toolbox/src/coreViem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createWalletClient, custom, rpcSchema } from 'viem'
import { addChain, CoreWalletAddChainParameters } from './overrides/addChain'
import { CoreWalletRpcSchema } from './rpcSchema'
import { isTestnet } from './methods/isTestnet'
import { getPChainAddress } from './methods/getPChainAddress'
import { createSubnet, CreateSubnetParams } from './methods/createSubnet'
import { createChain, CreateChainParams } from './methods/createChain'
import { convertToL1, ConvertToL1Params } from './methods/convertToL1'
import { extractWarpMessageFromPChainTx, ExtractWarpMessageFromTxParams } from './methods/extractWarpMessageFromPChainTx'
import { getEthereumChain } from './methods/getEthereumChain'
import { extractChainInfo, ExtractChainInfoParams } from './methods/extractChainInfo'
// import { sendTransaction } from './overrides/sendTransaction'

//Warning! This api is not stable yet, it will change in the future
export { type ConvertToL1Validator } from "./methods/convertToL1"

export function createCoreWalletClient(account: `0x${string}`) {
return createWalletClient({
transport: custom(window.avalanche!),
account: account,
rpcSchema: rpcSchema<CoreWalletRpcSchema>(),
}).extend((client) => ({
//override methods
addChain: (args: CoreWalletAddChainParameters) => addChain(client, args),
// sendTransaction: (args) => sendTransaction(client, args),

//new methods
isTestnet: () => isTestnet(client),
getPChainAddress: () => getPChainAddress(client),
createSubnet: (args: CreateSubnetParams) => createSubnet(client, args),
createChain: (args: CreateChainParams) => createChain(client, args),
convertToL1: (args: ConvertToL1Params) => convertToL1(client, args),
extractWarpMessageFromPChainTx: (args: ExtractWarpMessageFromTxParams) => extractWarpMessageFromPChainTx(client, args),
getEthereumChain: () => getEthereumChain(client),
extractChainInfo: (args: ExtractChainInfoParams) => extractChainInfo(client, args),
}))
}
90 changes: 90 additions & 0 deletions toolbox/src/coreViem/methods/convertToL1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { WalletClient } from "viem";
import {
L1Validator,
PChainOwner,
pvmSerial,
utils,
} from "@avalabs/avalanchejs";
import { CoreWalletRpcSchema } from "../rpcSchema";
import { isTestnet } from "./isTestnet";
import { getPChainAddress } from "./getPChainAddress";
import { getRPCEndpoint } from "../utils/rpc";
import { pvm } from "@avalabs/avalanchejs";
import { Context } from "@avalabs/avalanchejs";

export type ConvertToL1Params = {
managerAddress: string;
subnetId: string;
chainId: string;
subnetAuth: number[];
validators: ConvertToL1Validator[];
}

export type ConvertToL1Validator = {
nodeID: string;
nodePOP: {
publicKey: string;
proofOfPossession: string;
}
validatorWeight: bigint;
validatorBalance: bigint;
remainingBalanceOwner: ConvertToL1PChainOwner;
deactivationOwner: ConvertToL1PChainOwner;
}

export type ConvertToL1PChainOwner = {
addresses: string[];
threshold: number;
}

export async function convertToL1(client: WalletClient<any, any, any, CoreWalletRpcSchema>, params: ConvertToL1Params): Promise<string> {
const rpcEndpoint = getRPCEndpoint(await isTestnet(client));
const pvmApi = new pvm.PVMApi(rpcEndpoint);
const feeState = await pvmApi.getFeeState();
const context = await Context.getContextFromURI(rpcEndpoint);

const pChainAddress = await getPChainAddress(client);

const { utxos } = await pvmApi.getUTXOs({
addresses: [pChainAddress]
});

const validators: L1Validator[] = params.validators.map(validator => L1Validator.fromNative(
validator.nodeID,
BigInt(validator.validatorWeight),
BigInt(validator.validatorBalance),
new pvmSerial.ProofOfPossession(utils.hexToBuffer(validator.nodePOP.publicKey), utils.hexToBuffer(validator.nodePOP.proofOfPossession)),
PChainOwner.fromNative(
validator.remainingBalanceOwner.addresses.map(utils.bech32ToBytes),
validator.remainingBalanceOwner.threshold
),
PChainOwner.fromNative(
validator.deactivationOwner.addresses.map(utils.bech32ToBytes),
validator.deactivationOwner.threshold
)
));

const tx = pvm.e.newConvertSubnetToL1Tx(
{
feeState,
fromAddressesBytes: [utils.bech32ToBytes(pChainAddress)],
subnetId: params.subnetId,
utxos,
chainId: params.chainId,
validators,
subnetAuth: params.subnetAuth,
address: utils.hexToBuffer(params.managerAddress.replace('0x', '')),
},
context,
);

const transactionID = await window.avalanche!.request({
method: 'avalanche_sendTransaction',
params: {
transactionHex: utils.bufferToHex(tx.toBytes()),
chainAlias: 'P',
}
}) as string;

return transactionID;
}
55 changes: 55 additions & 0 deletions toolbox/src/coreViem/methods/createChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { WalletClient } from "viem";
import {
utils,
} from "@avalabs/avalanchejs";
import { CoreWalletRpcSchema } from "../rpcSchema";
import { isTestnet } from "./isTestnet";
import { getPChainAddress } from "./getPChainAddress";
import { getRPCEndpoint } from "../utils/rpc";
import { pvm } from "@avalabs/avalanchejs";
import { Context } from "@avalabs/avalanchejs";

export type CreateChainParams = {
chainName: string;
subnetAuth: number[];
subnetId: string;
vmId: string;
fxIds: string[];
genesisData: string;
}

export async function createChain(client: WalletClient<any, any, any, CoreWalletRpcSchema>, params: CreateChainParams): Promise<string> {
const rpcEndpoint = getRPCEndpoint(await isTestnet(client));
const pvmApi = new pvm.PVMApi(rpcEndpoint);
const feeState = await pvmApi.getFeeState();
const context = await Context.getContextFromURI(rpcEndpoint);

const pChainAddress = await getPChainAddress(client);

const { utxos } = await pvmApi.getUTXOs({
addresses: [pChainAddress]
});

const tx = pvm.e.newCreateChainTx({
feeState,
fromAddressesBytes: [utils.bech32ToBytes(pChainAddress)],
utxos,
chainName: params.chainName,
subnetAuth: params.subnetAuth,
subnetId: params.subnetId,
vmId: params.vmId,
fxIds: params.fxIds,
genesisData: JSON.parse(params.genesisData),
}, context);

const txID = await window.avalanche!.request({
method: 'avalanche_sendTransaction',
params: {
transactionHex: utils.bufferToHex(tx.toBytes()),
chainAlias: 'P',
}
}) as string;

return txID;

}
45 changes: 45 additions & 0 deletions toolbox/src/coreViem/methods/createSubnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { WalletClient } from "viem";
import {
utils,
} from "@avalabs/avalanchejs";
import { CoreWalletRpcSchema } from "../rpcSchema";
import { isTestnet } from "./isTestnet";
import { getPChainAddress } from "./getPChainAddress";
import { getRPCEndpoint } from "../utils/rpc";
import { pvm } from "@avalabs/avalanchejs";
import { Context } from "@avalabs/avalanchejs";

export type CreateSubnetParams = {
subnetOwners: string[];
}

export async function createSubnet(client: WalletClient<any, any, any, CoreWalletRpcSchema>, params: CreateSubnetParams): Promise<string> {
const rpcEndpoint = getRPCEndpoint(await isTestnet(client));
const pvmApi = new pvm.PVMApi(rpcEndpoint);
const feeState = await pvmApi.getFeeState();
const context = await Context.getContextFromURI(rpcEndpoint);

const pChainAddress = await getPChainAddress(client);

const { utxos } = await pvmApi.getUTXOs({
addresses: [pChainAddress]
});

const tx = pvm.e.newCreateSubnetTx({
feeState,
fromAddressesBytes: [utils.bech32ToBytes(pChainAddress)],
utxos,
subnetOwners: params.subnetOwners.map(utils.bech32ToBytes),
}, context);

const txID = await client.request({
method: 'avalanche_sendTransaction',
params: {
transactionHex: utils.bufferToHex(tx.toBytes()),
chainAlias: 'P',
}
}) as string;

return txID;

}
88 changes: 88 additions & 0 deletions toolbox/src/coreViem/methods/extractChainInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { WalletClient } from "viem";
import { getRPCEndpoint } from "../utils/rpc";
import { isTestnet } from "./isTestnet";
import { CoreWalletRpcSchema } from "../rpcSchema";

export type ExtractChainInfoParams = {
txId: string;
}

export type ExtractChainInfoResponse = {
subnetID: string;
chainName: string;
vmID: string;
genesisData: string;
}

// Define types for the API response structure
type PlatformGetTxResponse = {
jsonrpc: string;
result: {
tx: {
unsignedTx: {
networkID: number;
blockchainID: string;
subnetID: string;
chainName: string;
vmID: string;
genesisData: string;
// other fields exist but not needed for our use case
};
credentials: Array<{
signatures: string[];
}>;
id: string;
};
encoding: string;
};
id: number;
}

//TODO: rename
export async function extractChainInfo(client: WalletClient<any, any, any, CoreWalletRpcSchema>, { txId }: ExtractChainInfoParams): Promise<ExtractChainInfoResponse> {
const isTestnetMode = await isTestnet(client);
const rpcEndpoint = getRPCEndpoint(isTestnetMode);

//Fixme: here we do a direct call instead of using avalanchejs, because we need to get the raw response from the node
const response = await fetch(rpcEndpoint + "/ext/bc/P", {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'platform.getTx',
params: {
txID: txId,
encoding: 'json'
},
id: 1
})
});

const data = await response.json() as PlatformGetTxResponse | { error: { message: string } };

// Type guard to check if we have an error response
const isErrorResponse = (res: any): res is { error: { message: string } } => {
return 'error' in res && typeof res.error?.message === 'string';
};

if (isErrorResponse(data)) {
throw new Error(data.error.message);
}

if (!data.result) {
throw new Error("Received unexpected response from node: " + JSON.stringify(data).slice(0, 150));
}

console.log(data);
// Extract the relevant information from the response
const { subnetID, chainName, vmID, genesisData } = data.result.tx.unsignedTx;

return {
subnetID,
chainName,
vmID,
genesisData
};
}
Loading