Skip to content

Commit

Permalink
Add support for Solana (#12)
Browse files Browse the repository at this point in the history
* Add support for Solana

* Reference the IDL directly for strings

* Rename config

* Add payer to the signers

* Fix types
  • Loading branch information
r-near authored Dec 18, 2024
1 parent dd69661 commit 671ce0f
Show file tree
Hide file tree
Showing 8 changed files with 3,644 additions and 9 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@coral-xyz/anchor": "^0.30.1",
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.95.5",
"borsh": "^2.0.0",
"borsher": "^3.5.0",
Expand Down
305 changes: 305 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 1 addition & 8 deletions src/deployer/evm.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { ethers } from "ethers"
import type { ChainKind, MPCSignature, OmniAddress } from "../types"
import type { ChainKind, MPCSignature, OmniAddress, TokenMetadata } from "../types"
import { getChain } from "../utils"

// Type helpers for EVM chains
type EVMChainKind = typeof ChainKind.Eth | typeof ChainKind.Base | typeof ChainKind.Arb
type ChainTag<T extends ChainKind> = keyof T

interface TokenMetadata {
token: string
name: string
symbol: string
decimals: number
}

// Contract ABI for the bridge token factory
const BRIDGE_TOKEN_FACTORY_ABI = [
"function deployToken(bytes signatureData, tuple(string token, string name, string symbol, uint8 decimals) metadata) external returns (address)",
Expand Down
191 changes: 191 additions & 0 deletions src/deployer/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { Program, type Provider } from "@coral-xyz/anchor"
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"
import {
Keypair,
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
SystemProgram,
} from "@solana/web3.js"
import type { MPCSignature, TokenMetadata } from "../types"
import type { BridgeTokenFactory } from "../types/solana/bridge_token_factory"
import BRIDGE_TOKEN_FACTORY_IDL from "../types/solana/bridge_token_factory.json"

const MPL_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")

export class SolanaDeployer {
private readonly wormholeProgramId: PublicKey
private readonly program: Program<BridgeTokenFactory>

private static getConstant(name: string) {
const value = (BRIDGE_TOKEN_FACTORY_IDL as BridgeTokenFactory).constants.find(
(c) => c.name === name,
)?.value
if (!value) throw new Error(`Missing constant: ${name}`)
// Parse the string array format "[x, y, z]" into actual numbers
const numbers = JSON.parse(value as string)
return new Uint8Array(numbers)
}

private static readonly SEEDS = {
CONFIG: this.getConstant("CONFIG_SEED"),
AUTHORITY: this.getConstant("AUTHORITY_SEED"),
WRAPPED_MINT: this.getConstant("WRAPPED_MINT_SEED"),
VAULT: this.getConstant("VAULT_SEED"),
}

constructor(provider: Provider, wormholeProgramId: PublicKey) {
this.wormholeProgramId = wormholeProgramId
this.program = new Program(BRIDGE_TOKEN_FACTORY_IDL as BridgeTokenFactory, provider)
}

private config(): [PublicKey, number] {
return PublicKey.findProgramAddressSync([SolanaDeployer.SEEDS.CONFIG], this.program.programId)
}

private authority(): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[SolanaDeployer.SEEDS.AUTHORITY],
this.program.programId,
)
}

private wormholeBridgeId(): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("Bridge", "utf-8")],
this.wormholeProgramId,
)
}

private wormholeFeeCollectorId(): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("fee_collector", "utf-8")],
this.wormholeProgramId,
)
}

private wormholeSequenceId(): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from("Sequence", "utf-8"), this.config()[0].toBuffer()],
this.wormholeProgramId,
)
}

private wrappedMintId(token: string): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[SolanaDeployer.SEEDS.WRAPPED_MINT, Buffer.from(token, "utf-8")],
this.program.programId,
)
}

private vaultId(mint: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[SolanaDeployer.SEEDS.VAULT, mint.toBuffer()],
this.program.programId,
)
}

/**
* Logs metadata for a token
* @param token - The token's public key
* @param payer - Optional payer keypair
* @returns Promise resolving to transaction signature
*/
async logMetadata(token: PublicKey, payer?: Keypair): Promise<string> {
const wormholeMessage = Keypair.generate()
const [metadata] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata", "utf-8"), MPL_PROGRAM_ID.toBuffer(), token.toBuffer()],
MPL_PROGRAM_ID,
)
const [vault] = this.vaultId(token)

try {
const tx = await this.program.methods
.logMetadata()
.accountsStrict({
authority: this.authority()[0],
mint: token,
metadata,
vault,
wormhole: {
payer: payer?.publicKey || this.program.provider.publicKey,
config: this.config()[0],
bridge: this.wormholeBridgeId()[0],
feeCollector: this.wormholeFeeCollectorId()[0],
sequence: this.wormholeSequenceId()[0],
clock: SYSVAR_CLOCK_PUBKEY,
rent: SYSVAR_RENT_PUBKEY,
systemProgram: SystemProgram.programId,
wormholeProgram: this.wormholeProgramId,
message: wormholeMessage.publicKey,
},
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
})
.signers(payer instanceof Keypair ? [wormholeMessage, payer] : [wormholeMessage])
.rpc()

return tx
} catch (e) {
throw new Error(`Failed to log metadata: ${e}`)
}
}

/**
* Deploys a new wrapped token
* @param signature - MPC signature authorizing the deployment
* @param tokenMetadata - Token metadata
* @param payer - Optional payer public key
* @returns Promise resolving to transaction hash and token address
*/
async deployToken(
signature: MPCSignature,
payload: TokenMetadata,
payer?: Keypair,
): Promise<{ txHash: string; tokenAddress: string }> {
const wormholeMessage = Keypair.generate()
const [mint] = this.wrappedMintId(payload.token)
const [metadata] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata", "utf-8"), MPL_PROGRAM_ID.toBuffer(), mint.toBuffer()],
MPL_PROGRAM_ID,
)

try {
const tx = await this.program.methods
.deployToken({
payload,
signature: signature.toBytes(),
})
.accountsStrict({
authority: this.authority()[0],
wormhole: {
payer: payer?.publicKey || this.program.provider.publicKey,
config: this.config()[0],
bridge: this.wormholeBridgeId()[0],
feeCollector: this.wormholeFeeCollectorId()[0],
sequence: this.wormholeSequenceId()[0],
clock: SYSVAR_CLOCK_PUBKEY,
rent: SYSVAR_RENT_PUBKEY,
systemProgram: SystemProgram.programId,
wormholeProgram: this.wormholeProgramId,
message: wormholeMessage.publicKey,
},
metadata,
systemProgram: SystemProgram.programId,
mint,
tokenProgram: TOKEN_PROGRAM_ID,
tokenMetadataProgram: MPL_PROGRAM_ID,
})
.signers(payer instanceof Keypair ? [wormholeMessage, payer] : [wormholeMessage])
.rpc()

return {
txHash: tx,
tokenAddress: mint.toString(),
}
} catch (e) {
throw new Error(`Failed to deploy token: ${e}`)
}
}
}
7 changes: 7 additions & 0 deletions src/types/omni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ export enum Status {
Completed = 1,
Failed = 2,
}

export interface TokenMetadata {
token: string
name: string
symbol: string
decimals: number
}
Loading

0 comments on commit 671ce0f

Please sign in to comment.