Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
SUI_PRIVATE_KEY=
XRPL_SEED=
ENVIRONMENT=testnet
SOLANA_PRIVATE_KEY=
ENVIRONMENT=testnet
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,23 @@ Example:
```bash
bun xrpl:trust-line SQD 10000
```

## Solana ITS Transfer

Send example token (currently set to f238f2d38d16c5f472629c401433ccb704eea5e9d1aa8bb8e75e3628db65dc65) from Solana to other chain:

```bash
bun solana:start <token-id> <token-address> <destination-chain> <destination-address> <amount> <payload>
```

Example:

```bash
bun solana:start f238f2d38d16c5f472629c401433ccb704eea5e9d1aa8bb8e75e3628db65dc65 8yqTiWbBsd82Lcnw3wJpX2mTVPxyftKKoMt2hCA2m2wS avalanche-fuji 0x5eaF5407acc1be854644BE2Be20Ac23D07e491D6 1 "Hello from Solana"
```

> Note: the destination address and amount are optional.
> There is no "gas price" on Solana - transaction fees are fixed, and there is an optional priority fee.
> The script will display the average prioritization fee as returned by the RPC.
> Specifying a payload is optional.
> Example GMP call: https://devnet-amplifier.axelarscan.io/gmp/5UWgw1wpveXdaKwGnWzLmzhpbC4gSB9Pu2s5orR4wjLtKCtnMygikWdFRVuv37ze1Hnf9ubVEUG4FhcUYbKKqzzx-15
Binary file modified bun.lockb
Binary file not shown.
12 changes: 11 additions & 1 deletion common/chains.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SuiChainConfig, XrplChainConfig } from "./types";
import type { SuiChainConfig, XrplChainConfig, SolanaChainConfig } from "./types";
import { environment } from "../common/env";

export async function getChainConfig() {
Expand Down Expand Up @@ -32,3 +32,13 @@ export async function getXrplChainConfig(): Promise<XrplChainConfig> {

return chainConfig.chains[xrplChainId];
}

export async function getSolanaChainConfig(): Promise<SolanaChainConfig> {
const chainConfig = await getChainConfig();

const solanaChainId = Object.keys(chainConfig.chains).find(
(chain) => chainConfig.chains[chain].id === "solana-12",
) as string;

return chainConfig.chains[solanaChainId];
}
5 changes: 5 additions & 0 deletions common/gasEstimation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { AxelarQueryAPI } from "@axelar-network/axelarjs-sdk";
import { environment } from "../common/env";
import { isPartOfTypeOnlyImportOrExportDeclaration } from "typescript";

export type HopParams = {
sourceChain: string;
destinationChain: string;
gasLimit: string;
executeData?: string;
};

export async function calculateEstimatedFee(
sourceChain: string,
destinationChain: string,
payload: string | null,
): Promise<string> {
const sdk = new AxelarQueryAPI({
environment,
Expand All @@ -20,11 +23,13 @@ export async function calculateEstimatedFee(
sourceChain: sourceChain,
destinationChain: "axelar",
gasLimit: "400000",
executeData: payload ?? undefined,
},
{
sourceChain: "axelar",
destinationChain: destinationChain,
gasLimit: "1100000",
executeData: payload ?? undefined,
},
];
const amount = (await sdk.estimateMultihopFee(hopParams)) as string;
Expand Down
10 changes: 10 additions & 0 deletions common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ export type XrplChainConfig = {
contracts: Record<string, BaseContract>;
};
};

export type SolanaChainConfig = {
id: string;
chainType: string;
blockExplorers: BlockExplorer[];
config: {
rpc: string[];
contracts: Record<string, BaseContract>;
};
};
6 changes: 6 additions & 0 deletions gen-solana-wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Keypair } from "@solana/web3.js";

const keypair = Keypair.generate();

console.log("Wallet Address:", keypair.publicKey.toBase58());
console.log("Secret key", JSON.stringify(Array.from(keypair.secretKey)));
16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
"sui:start": "bun run sui.ts",
"xrpl:wallet": "bun run gen-xrpl-wallet.ts",
"xrpl:start": "bun run xrpl.ts",
"xrpl:trust-line": "bun run xrpl-trust-line.ts"
"xrpl:trust-line": "bun run xrpl-trust-line.ts",
"solana:wallet": "bun run gen-solana-wallet.ts",
"solana:start": "bun run solana.ts"
},
"peerDependencies": {
"typescript": "^5.0.0"
"typescript": "^5.8.2"
},
"dependencies": {
"@axelar-network/axelarjs-sdk": "^0.17.2",
"@aws-crypto/sha256-js": "^5.2.0",
"@axelar-network/axelarjs-sdk": "^0.17.5",
"@axelarjs/sui": "0.0.0-snapshot.8996e1c",
"@coral-xyz/anchor": "^0.31.1",
"@mysten/sui": "~1.24.0",
"ethers": "^6.13.5",
"@solana/spl-token": "^0.4.14",
"@solana/web3.js": "^1.98.4",
"borsh": "^2.0.0",
"borsh-js": "^0.0.1-security",
"ethers": "^5.8.0",
"ripple-address-codec": "^5.0.0",
"xrpl": "4.2.0"
}
Expand Down
171 changes: 171 additions & 0 deletions solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { getSolanaChainConfig } from "./common/chains";
import { getSolanaKeypair } from "./solana/wallet";
import { calculateEstimatedFee } from "./common/gasEstimation";
import { environment } from "./common/env";
import { Connection, clusterApiUrl, LAMPORTS_PER_SOL, sendAndConfirmTransaction, type Cluster } from "@solana/web3.js";
import { fundWallet } from "./solana/wallet";
import { buildInterchainTransferTx } from "./solana/tokenOperations";
import { type GMPCallInput, type InterchainTransferInput } from "./solana/types";
import { buildCallContractTx } from "./solana/sendGMP";

// --- Constants ---
const TOKEN_ID: string = process.argv[2] || "f238f2d38d16c5f472629c401433ccb704eea5e9d1aa8bb8e75e3628db65dc65";
const TOKEN_ADDRESS: string = process.argv[3] || "8yqTiWbBsd82Lcnw3wJpX2mTVPxyftKKoMt2hCA2m2wS";
const DESTINATION_CHAIN: string = process.argv[4] || "avalanche-fuji";
const DESTINATION_ADDRESS: string = process.argv[5] || "0xA57ADCE1d2fE72949E4308867D894CD7E7DE0ef2";
const AMOUNT = process.argv[6] || "1";
let PAYLOAD = process.argv[7] || null;
let payloadBytes = new Uint8Array();

if (PAYLOAD !== null) {
if (PAYLOAD.slice(0, 2) == "0x") {
// hex decode this
payloadBytes = Uint8Array.fromHex(PAYLOAD.slice(2));
} else {
payloadBytes = Uint8Array.from(PAYLOAD);
}
}

if(payloadBytes.length > 530) {
throw new Error("Payload too long! Consider using off-chain data");
}

// translate environment into one of the Solana clusters
// Possible options: 'devnet' | 'testnet' | 'mainnet-beta'
const configMapping = {
"devnet-amplifier": {solanaCluster: "devnet", axelarscanUrl: "https://devnet-amplifier.axelarscan.io"},
"testnet": {solanaCluster: "testnet", axelarscanUrl: "https://testnet.axelarscan.io"},
"mainnet": {solanaCluster: "mainnet-beta", axelarscanUrl: "https://axelarscan.io"},
};

console.log("Environment:", environment);
if (!(environment in configMapping)) {
throw new Error("Invalid environment");
}
const {solanaCluster, axelarscanUrl} = configMapping[environment];


const SOLANA_CONFIG = await getSolanaChainConfig();
const SOLANA_CONTRACTS = SOLANA_CONFIG.config.contracts;

const { InterchainTokenService, AxelarGateway, AxelarGasService } = SOLANA_CONTRACTS;

const connection = new Connection(clusterApiUrl(solanaCluster as Cluster), "confirmed");

// get balance of the current wallet
const keypair = getSolanaKeypair();

let balance = await connection.getBalance(keypair.publicKey);
console.log("Current wallet balance is:", balance, "lamports (", balance / LAMPORTS_PER_SOL, "SOL)");

if (balance < 1 * LAMPORTS_PER_SOL && (environment == "testnet" || environment == "devnet-amplifier")) {
// attempt to refund wallet if we are on a testnet
balance = await fundWallet(connection, keypair);
console.log("Balance after funding wallet is", balance);
}

// SOLANA GAS ESTIMATION
// For every signature on the transaction, the transaction needs to pay 5000 lamports (with 10^9 lamports = 1 SOL)

// There is an optional priority fee, which we query now -> this is equivalent to the "gas price" when there is high demand
// This example is taken from https://docs.chainstack.com/docs/solana-estimate-priority-fees-getrecentprioritizationfees
interface PrioritizationFeeObject {
slot: number;
prioritizationFee: number;
}

const prioritizationFeeObjects = await connection.getRecentPrioritizationFees() as PrioritizationFeeObject[];

if (prioritizationFeeObjects.length === 0) {
console.log('No prioritization fee data available.');
throw new Error("Failed to get prioritization fee data");
}

const slots = prioritizationFeeObjects.map(feeObject => feeObject.slot).sort((a, b) => a - b);

// Extract slots range
const slotsRangeStart = slots[0];
const slotsRangeEnd = slots[slots.length - 1];

// Calculate the average including zero fees
const averageFeeIncludingZeros = prioritizationFeeObjects.length > 0
? Math.floor(prioritizationFeeObjects.reduce((acc, feeObject) => acc + feeObject.prioritizationFee, 0) / prioritizationFeeObjects.length)
: 0;

// Filter out prioritization fees that are equal to 0 for other calculations
const nonZeroFees = prioritizationFeeObjects
.map(feeObject => feeObject.prioritizationFee)
.filter(fee => fee !== 0);

// Calculate the average of the non-zero fees
const averageFeeExcludingZeros = nonZeroFees.length > 0
? Math.floor(nonZeroFees.reduce((acc, fee) => acc + fee, 0) / nonZeroFees.length )
: 0;

// Calculate the median of the non-zero fees
const sortedFees = nonZeroFees.sort((a, b) => a - b);
let medianFee = 0;
if (sortedFees.length > 0) {
const midIndex = Math.floor(sortedFees.length / 2);
medianFee = sortedFees.length % 2 !== 0
? sortedFees[midIndex]
: Math.floor((sortedFees[midIndex - 1] + sortedFees[midIndex]) / 2);
}

console.log(`Slots examined for priority fees: ${prioritizationFeeObjects.length}`)
console.log(`Slots range examined from ${slotsRangeStart} to ${slotsRangeEnd}`);
console.log('====================================================================================')

// You can use averageFeeIncludingZeros, averageFeeExcludingZeros, and medianFee in your transactions script
console.log(` 💰 Average Prioritization Fee (including slots with zero fees): ${averageFeeIncludingZeros} micro-lamports.`);
console.log(` 💰 Average Prioritization Fee (excluding slots with zero fees): ${averageFeeExcludingZeros} micro-lamports.`);
console.log(` 💰 Median Prioritization Fee (excluding slots with zero fees): ${medianFee} micro-lamports.`);
// Done with reading prioritization fee for Solana

// Calculating how much gas we have to pay for the ITS transfer to go through.
const GAS = await calculateEstimatedFee(SOLANA_CONFIG.id, DESTINATION_CHAIN, PAYLOAD);
console.log("Estimated gas as", GAS);

let tx;
let axelarscanIndex;

if (TOKEN_ID == "0x") {
console.log("Performing pure GMP call instead of ITS transfer");
const params = {
caller: keypair.publicKey.toString(),
destinationChain: DESTINATION_CHAIN,
destinationAddress: DESTINATION_ADDRESS,
gasValue: GAS,
priorityFee: averageFeeIncludingZeros, // use the average priority fee
payload: payloadBytes,
} as GMPCallInput;

tx = await buildCallContractTx(params);
axelarscanIndex = '1.1';
} else {
const params = {
caller: keypair.publicKey.toString(),
tokenId: TOKEN_ID,
tokenAddress: TOKEN_ADDRESS,
destinationChain: DESTINATION_CHAIN,
destinationAddress: DESTINATION_ADDRESS,
amount: AMOUNT,
gasValue: GAS,
priorityFee: averageFeeIncludingZeros, // use the average priority fee
payload: payloadBytes,
} as InterchainTransferInput;

tx = await buildInterchainTransferTx(params);
axelarscanIndex = '2.7';
}

tx.sign(keypair);

const signature = await sendAndConfirmTransaction(connection, tx, [
keypair,
]);

console.log("Sent transaction!", signature);
console.log("View it on the explorer: https://explorer.solana.com/tx/" + signature + '?cluster=' + solanaCluster);
console.log("View it on Axelarscan: " + axelarscanUrl + "/gmp/" + signature + '-' + axelarscanIndex);

Loading