pyth-crosschain/web/src/utils/bridge.ts

395 lines
12 KiB
TypeScript

import * as solanaWeb3 from "@solana/web3.js";
import {PublicKey, TransactionInstruction} from "@solana/web3.js";
import BN from 'bn.js';
import assert from "assert";
// @ts-ignore
import * as BufferLayout from 'buffer-layout'
import {Token} from "@solana/spl-token";
import {TOKEN_PROGRAM} from "../config";
import * as bs58 from "bs58";
export interface AssetMeta {
chain: number,
address: Buffer
}
export interface Lockup {
amount: BN,
toChain: number,
sourceAddress: PublicKey,
targetAddress: Uint8Array,
assetAddress: Uint8Array,
assetChain: number,
nonce: number,
vaa: Uint8Array,
vaaTime: number,
initialized: boolean,
}
export const CHAIN_ID_SOLANA = 1;
class SolanaBridge {
connection: solanaWeb3.Connection;
programID: PublicKey;
tokenProgram: PublicKey;
constructor(connection: solanaWeb3.Connection, programID: PublicKey, tokenProgram: PublicKey) {
this.programID = programID;
this.tokenProgram = tokenProgram;
this.connection = connection;
}
async createLockAssetInstruction(
payer: PublicKey,
tokenAccount: PublicKey,
mint: PublicKey,
amount: BN,
targetChain: number,
targetAddress: Buffer,
asset: AssetMeta,
nonce: number,
): Promise<TransactionInstruction> {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint256('amount'),
BufferLayout.u8('targetChain'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.u8('assetDecimals'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.seq(BufferLayout.u8(), 1),
BufferLayout.u32('nonce'),
]);
let nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32LE(nonce, 0);
// @ts-ignore
let configKey = await this.getConfigKey();
let seeds: Array<Buffer> = [Buffer.from("transfer"), configKey.toBuffer(), new Buffer([asset.chain]),
padBuffer(asset.address, 32), new Buffer([targetChain]), padBuffer(targetAddress, 32), tokenAccount.toBuffer(),
nonceBuffer,
];
// @ts-ignore
let transferKey = (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 1, // TransferOut instruction
amount: padBuffer(new Buffer(amount.toArray()), 32),
targetChain: targetChain,
assetAddress: padBuffer(asset.address, 32),
assetChain: asset.chain,
assetDecimals: 0, // This is fetched on chain
targetAddress: padBuffer(targetAddress, 32),
nonce: nonce,
},
data,
);
const keys = [
{pubkey: this.programID, isSigner: false, isWritable: false},
{pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false},
{pubkey: this.tokenProgram, isSigner: false, isWritable: false},
{pubkey: tokenAccount, isSigner: false, isWritable: true},
{pubkey: configKey, isSigner: false, isWritable: false},
{pubkey: transferKey, isSigner: false, isWritable: true},
{pubkey: mint, isSigner: false, isWritable: false},
{pubkey: payer, isSigner: true, isWritable: true},
];
if (asset.chain == CHAIN_ID_SOLANA) {
// @ts-ignore
let custodyKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("custody"), configKey.toBuffer(), mint.toBuffer()], this.programID))[0];
keys.push({pubkey: custodyKey, isSigner: false, isWritable: true})
}
return new TransactionInstruction({
keys,
programId: this.programID,
data,
});
}
// fetchAssetMeta fetches the AssetMeta for an SPL token
async fetchAssetMeta(
mint: PublicKey,
): Promise<AssetMeta> {
// @ts-ignore
let configKey = await this.getConfigKey();
let seeds: Array<Buffer> = [Buffer.from("meta"), configKey.toBuffer(), mint.toBuffer()];
// @ts-ignore
let metaKey = (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
let metaInfo = await this.connection.getAccountInfo(metaKey);
if (metaInfo == null || metaInfo.lamports == 0) {
return {
address: mint.toBuffer(),
chain: CHAIN_ID_SOLANA,
}
} else {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('assetChain'),
BufferLayout.blob(32, 'assetAddress'),
]);
let wrappedMeta = dataLayout.decode(metaInfo?.data);
return {
address: wrappedMeta.assetAddress,
chain: wrappedMeta.assetChain
}
}
}
// fetchAssetMeta fetches the AssetMeta for an SPL token
async fetchTransferProposals(
tokenAccount: PublicKey,
): Promise<Lockup[]> {
let accountRes = await fetch("http://localhost:8899", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [this.programID.toString(), {
"filters": [{"dataSize": 1152}, {
"memcmp": {
"offset": 33,
"bytes": tokenAccount.toString()
}
}]
}]
}),
})
let raw_accounts = (await accountRes.json())["result"];
const dataLayout = BufferLayout.struct([
uint256('amount'),
BufferLayout.u8('toChain'),
BufferLayout.blob(32, 'sourceAddress'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.u8('assetDecimals'),
BufferLayout.u32('nonce'),
BufferLayout.blob(1001, 'vaa'),
BufferLayout.u32('vaaTime'),
BufferLayout.u8('initialized'),
]);
let accounts: Lockup[] = [];
for (let acc of raw_accounts) {
acc = acc.account;
let parsedAccount = dataLayout.decode(bs58.decode(acc.data))
console.log(parsedAccount);
accounts.push({
amount: new BN(parsedAccount.amount, 2, "le"),
assetAddress: parsedAccount.assetAddress,
assetChain: acc.assetChain,
initialized: acc.initialized == 1,
nonce: acc.nonce,
sourceAddress: new PublicKey(parsedAccount.sourceAddress),
targetAddress: parsedAccount.targetAddress,
toChain: acc.toChain,
vaa: acc.vaa,
vaaTime: acc.vaaTime
})
}
return accounts
}
AccountLayout = BufferLayout.struct([publicKey('mint'), publicKey('owner'), uint64('amount'), BufferLayout.u32('option'), publicKey('delegate'), BufferLayout.u8('is_initialized'), BufferLayout.u8('is_native'), BufferLayout.u16('padding'), uint64('delegatedAmount')]);
async createWrappedAssetAndAccountInstructions(owner: PublicKey, mint: PublicKey): Promise<[TransactionInstruction[], solanaWeb3.Account]> {
const newAccount = new solanaWeb3.Account();
// @ts-ignore
const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(this.connection);
let transaction = solanaWeb3.SystemProgram.createAccount({
fromPubkey: owner,
newAccountPubkey: newAccount.publicKey,
lamports: balanceNeeded,
space: this.AccountLayout.span,
programId: TOKEN_PROGRAM,
}); // create the new account
const keys = [{
pubkey: newAccount.publicKey,
isSigner: false,
isWritable: true
}, {
pubkey: mint,
isSigner: false,
isWritable: false
}, {
pubkey: owner,
isSigner: false,
isWritable: false
}];
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode({
instruction: 1 // InitializeAccount instruction
}, data);
let ix_init = {
keys,
programId: TOKEN_PROGRAM,
data
}
return [[transaction.instructions[0], ix_init], newAccount]
}
async getConfigKey(): Promise<PublicKey> {
// @ts-ignore
return (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("bridge")], this.programID))[0]
}
async getWrappedAssetMint(asset: AssetMeta): Promise<PublicKey> {
if (asset.chain === 1) {
return new PublicKey(asset.address)
}
let configKey = await this.getConfigKey();
let seeds: Array<Buffer> = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.of(asset.chain),
padBuffer(asset.address, 32)];
// @ts-ignore
return (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
}
}
// Taken from https://github.com/solana-labs/solana-program-library
// Licensed under Apache 2.0
export class u64 extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 8) {
return b;
}
assert(b.length < 8, 'u64 too large');
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a u64 from Buffer representation
*/
static fromBuffer(buffer: Buffer): u64 {
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
return new BN(
// @ts-ignore
[...buffer]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
}
}
function padBuffer(b: Buffer, len: number): Buffer {
const zeroPad = Buffer.alloc(len);
b.copy(zeroPad, len - b.length);
return zeroPad;
}
export class u256 extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 32) {
return b;
}
assert(b.length < 32, 'u256 too large');
const zeroPad = Buffer.alloc(32);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a u256 from Buffer representation
*/
static fromBuffer(buffer: number[]): u256 {
assert(buffer.length === 32, `Invalid buffer length: ${buffer.length}`);
return new BN(
// @ts-ignore
[...buffer]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
}
}
/**
* Layout for a public key
*/
export const publicKey = (property: string = 'publicKey'): Object => {
return BufferLayout.blob(32, property);
};
/**
* Layout for a 64bit unsigned value
*/
export const uint64 = (property: string = 'uint64'): Object => {
return BufferLayout.blob(8, property);
};
/**
* Layout for a 256-bit unsigned value
*/
export const uint256 = (property: string = 'uint256'): Object => {
return BufferLayout.blob(32, property);
};
/**
* Layout for a Rust String type
*/
export const rustString = (property: string = 'string') => {
const rsl = BufferLayout.struct(
[
BufferLayout.u32('length'),
BufferLayout.u32('lengthPadding'),
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
],
property,
);
const _decode = rsl.decode.bind(rsl);
const _encode = rsl.encode.bind(rsl);
rsl.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return data.chars.toString('utf8');
};
rsl.encode = (str: string, buffer: Buffer, offset: number) => {
const data = {
chars: Buffer.from(str, 'utf8'),
};
return _encode(data, buffer, offset);
};
return rsl;
};
export {SolanaBridge}