mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
Adds SOLAssembler — companion to the EVM Assembler — that constructs
Solana transactions for the bridge's destination-release flow:
- PreSign: fetch a recent blockhash via JSON-RPC getLatestBlockhash,
build a v0 message (one signer = the MPC release wallet as fee
payer, one recipient, lamport amount), serialize the bytes to be
signed. SPL token mode (when SourceMint is set) derives source +
destination ATAs via solana.FindAssociatedTokenAddress and probes
the destination ATA via getAccountInfo — if absent, the assembler
prepends a CreateAssociatedTokenAccount instruction so the release
wallet pays the ~2_039_280 lamport rent + creates the account.
- Finalize: slot the 64-byte Ed25519 signature into Transaction
.Signatures[0], serialize the full wire tx with MarshalBinary, and
base64-encode for JSON-RPC sendTransaction (encoding:base64). The
signature itself doubles as the canonical Solana "tx hash" — return
it base58-encoded for SDK consumption.
Dependency: github.com/gagliardetto/solana-go v1.20.0 — the de facto
standard Go client. The assembler stays decoupled from the broader
*rpc.Client beyond GetLatestBlockhash + GetAccountInfo, so the
broadcast layer can use its own JSON-RPC endpoint.
Helpers:
- LamportsFromFloat(amount, decimals): safe float → uint64 conversion
with overflow protection. 9dp for native SOL; varies for SPL.
- ParseSOLSignatureHex(s): decodes a 128-char hex string to a fixed
[64]byte buffer — used by the signing driver when the dashboard
returns the Ed25519 sig in hex form.
- SOLDefaultBlockhashURL(network): canonical RPC URL per network.
Tests (sol_test.go) cover:
- Native transfer happy path: instruction is system.Transfer at
Account[0] = payer; round-trip of MessageBytes through
Message.Unmarshal yields an identical message.
- SPL transfer with destination ATA missing: 2 instructions
(ata.Create, then token.Transfer), destination ATA correctly
derived and probed.
- SPL transfer with destination ATA present: only 1 instruction.
- Finalize round-trip: rawTx parses back through solana-go's
Transaction decoder with matching signature + payer + blockhash.
- Error paths: unknown network, zero amount, malformed payer addr.
- ParseSOLSignatureHex + LamportsFromFloat: corner cases.
Total: 12 SOL tests, all green.
475 lines
18 KiB
Go
475 lines
18 KiB
Go
// sol.go: Solana destination-chain transaction assembly. Companion to
|
|
// the EVM assembler in assembler.go but expressed in terms of Solana's
|
|
// own primitives (instructions, message, accounts, signatures).
|
|
//
|
|
// What we build:
|
|
// - Legacy / v0 messages: one fee payer (the MPC release wallet), one
|
|
// recipient, lamport amount (native SOL transfer) OR SPL token
|
|
// transfer (source ATA → destination ATA, with ATA creation when
|
|
// the destination ATA doesn't exist).
|
|
// - No address-lookup-tables. ALT support is a follow-up; the bridge
|
|
// only needs the basic shape for its release path today.
|
|
//
|
|
// Two-step ceremony — same shape as the EVM path:
|
|
// 1. PreSign: build the unsigned message, compute the bytes the MPC
|
|
// cluster will sign over (= raw message bytes; Solana doesn't hash
|
|
// before signing — Ed25519 hashes internally as part of the
|
|
// signing scheme).
|
|
// 2. Finalize: prepend the 64-byte signature in the canonical place
|
|
// and serialize the wire-ready transaction.
|
|
//
|
|
// The Solana wire transaction layout is:
|
|
//
|
|
// [compactU16: numSignatures]
|
|
// [64 bytes * numSignatures] ← signatures, in account_keys order
|
|
// [message bytes] ← header + accounts + recent_blockhash + instructions
|
|
//
|
|
// For the bridge's release tx, numSignatures is always 1 (the MPC
|
|
// release wallet is the sole signer + fee payer). The message bytes are
|
|
// what the MPC cluster signs. PreSign returns those bytes verbatim so
|
|
// the signing driver can hex-encode them and ship them to the cluster.
|
|
|
|
package txassembler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/gagliardetto/solana-go"
|
|
ata "github.com/gagliardetto/solana-go/programs/associated-token-account"
|
|
"github.com/gagliardetto/solana-go/programs/system"
|
|
"github.com/gagliardetto/solana-go/programs/token"
|
|
"github.com/gagliardetto/solana-go/rpc"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Public types
|
|
// =============================================================================
|
|
|
|
// SOLNetworkConfig is the bridge-side config for one Solana network
|
|
// (mainnet vs devnet). Used to resolve the RPC endpoint that fetches
|
|
// the recent blockhash. Distinct from broadcast's RPC endpoint —
|
|
// callers can point them at different nodes (e.g. authenticated paid
|
|
// provider for sendTransaction, public RPC for blockhash polling).
|
|
type SOLNetworkConfig struct {
|
|
// BlockhashURL is the JSON-RPC endpoint used for getLatestBlockhash
|
|
// + getAccountInfo (ATA existence check). Required.
|
|
BlockhashURL string
|
|
|
|
// Commitment selects the freshness level for getLatestBlockhash.
|
|
// Solana defaults to "finalized"; "confirmed" gets a fresher hash
|
|
// at the cost of a slightly higher BlockhashNotFound risk. The
|
|
// bridge picks "confirmed" — release txs land within 1-2 slots so
|
|
// a 1-2 slot newer hash isn't a risk vs. a hash that may already
|
|
// be ~1 minute old by the time the MPC ceremony finishes.
|
|
Commitment rpc.CommitmentType
|
|
}
|
|
|
|
// SOLSpec is the Solana destination intent. Companion to SwapIntent
|
|
// for the EVM path — kept separate because Solana's parameters are
|
|
// fundamentally different (no nonce, no gas price, no chainID; just
|
|
// recent blockhash + lamports + accounts).
|
|
//
|
|
// Native SOL transfer (SourceMint empty): system.NewTransferInstruction
|
|
// from PayerAddress to RecipientAddress for LamportsAmount.
|
|
//
|
|
// SPL token transfer (SourceMint set): token.NewTransferInstruction
|
|
// from the source ATA (derived from PayerAddress + SourceMint) to the
|
|
// destination ATA (derived from RecipientAddress + SourceMint). When
|
|
// the destination ATA doesn't exist yet, the assembler prepends a
|
|
// CreateAssociatedTokenAccount instruction to mint it on the fly.
|
|
// Payer wallet covers both the transfer fee + the ATA rent-exempt
|
|
// minimum (~2_039_280 lamports as of 2026-05).
|
|
type SOLSpec struct {
|
|
// Network is the bridge internal network name, e.g. "SOLANA_DEVNET".
|
|
Network string
|
|
|
|
// PayerAddress is the release wallet's base58-encoded Ed25519
|
|
// public key — fees, fee payer signature, source of value.
|
|
PayerAddress string
|
|
|
|
// RecipientAddress is the destination wallet (user). Base58.
|
|
RecipientAddress string
|
|
|
|
// LamportsAmount is the value to transfer in lamports. For SPL,
|
|
// this is interpreted in the token's base units (not lamports);
|
|
// Solana doesn't distinguish "wei" vs "lamports" in the program
|
|
// arg — both system.Transfer and token.Transfer take uint64.
|
|
LamportsAmount uint64
|
|
|
|
// SourceMint, when non-empty, switches the assembler into SPL
|
|
// token transfer mode. Base58 mint address. Empty = native SOL.
|
|
SourceMint string
|
|
}
|
|
|
|
// SOLUnsigned is the intermediate representation produced by PreSign.
|
|
// Companion to *Unsigned for the EVM path. The signing driver feeds
|
|
// MessageBytes to the MPC cluster as the message-to-sign (hex), then
|
|
// passes the resulting 64-byte signature back through Finalize along
|
|
// with this struct to produce the wire-ready raw transaction.
|
|
//
|
|
// Why expose the *Transaction directly: solana-go's transaction has
|
|
// no public "set signatures" method, so Finalize has to hold a live
|
|
// handle to the tx to slot the signature in. Keeping it on Unsigned
|
|
// keeps the assembler stateless between calls.
|
|
type SOLUnsigned struct {
|
|
// Network mirrors SOLSpec.Network — useful when downstream code
|
|
// needs to route by network without holding the SOLSpec.
|
|
Network string
|
|
|
|
// Blockhash is the recent_blockhash baked into Message. Surfaced
|
|
// here so the signing driver can stash it on the swap record and
|
|
// detect BlockhashNotFound errors at broadcast time (the message
|
|
// must be rebuilt + re-signed with a fresher hash).
|
|
Blockhash string
|
|
|
|
// MessageBytes is the raw, canonical wire-encoded message — i.e.
|
|
// exactly what the Ed25519 signing function will hash + sign over.
|
|
// Sent to the MPC cluster as the message-to-sign.
|
|
MessageBytes []byte
|
|
|
|
// tx is the in-flight transaction. solana-go encodes signatures +
|
|
// message into the wire format via Transaction.MarshalBinary, so
|
|
// Finalize sets tx.Signatures[0] and re-marshals.
|
|
tx *solana.Transaction
|
|
|
|
// payer is captured so Finalize can confirm the signing driver
|
|
// hasn't fed it a signature from the wrong key.
|
|
payer solana.PublicKey
|
|
}
|
|
|
|
// SOLAssembler is the Solana-side counterpart to *Assembler. It speaks
|
|
// to a Solana RPC for blockhash + account-info lookups and produces
|
|
// SOLUnsigned values for the signing driver.
|
|
type SOLAssembler struct {
|
|
// Networks maps internal-name → per-network config. PreSign rejects
|
|
// unknown networks rather than silently defaulting.
|
|
Networks map[string]SOLNetworkConfig
|
|
|
|
// RPCFactory builds a solana-go *rpc.Client for the given URL.
|
|
// Kept as a field so tests can stub the network round-trip. Nil ⇒
|
|
// use rpc.New (the production constructor).
|
|
RPCFactory func(url string) *rpc.Client
|
|
}
|
|
|
|
// NewSOLAssembler builds an assembler with sensible defaults. Networks
|
|
// can be populated via SetNetwork.
|
|
func NewSOLAssembler() *SOLAssembler {
|
|
return &SOLAssembler{
|
|
Networks: map[string]SOLNetworkConfig{},
|
|
}
|
|
}
|
|
|
|
// SetNetwork registers a per-network config.
|
|
func (a *SOLAssembler) SetNetwork(network string, cfg SOLNetworkConfig) {
|
|
if cfg.Commitment == "" {
|
|
cfg.Commitment = rpc.CommitmentConfirmed
|
|
}
|
|
a.Networks[network] = cfg
|
|
}
|
|
|
|
// rpcFor builds (or stubs) the rpc.Client for the given URL.
|
|
func (a *SOLAssembler) rpcFor(url string) *rpc.Client {
|
|
if a.RPCFactory != nil {
|
|
return a.RPCFactory(url)
|
|
}
|
|
return rpc.New(url)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PreSign
|
|
// =============================================================================
|
|
|
|
// ErrSOLNoNetworkConfig is returned by PreSign when SOLSpec.Network
|
|
// isn't in SOLAssembler.Networks. Callers must register the network
|
|
// (network YAML + bridge startup wiring) before PreSign can run.
|
|
var ErrSOLNoNetworkConfig = errors.New("txassembler: no SOL network config for that network")
|
|
|
|
// PreSign builds the unsigned Solana transaction. The returned
|
|
// SOLUnsigned.MessageBytes is the bytes-to-sign — feed it to the MPC
|
|
// cluster as the Ed25519 message.
|
|
//
|
|
// The flow:
|
|
// 1. Parse PayerAddress + RecipientAddress + (optional) SourceMint.
|
|
// 2. Fetch the recent blockhash from the network's RPC.
|
|
// 3. Build instructions:
|
|
// - Native: [system.Transfer(payer, recipient, lamports)]
|
|
// - SPL with destination ATA present:
|
|
// [token.Transfer(srcATA, dstATA, payer, lamports)]
|
|
// - SPL with destination ATA absent:
|
|
// [ata.Create(payer, recipient, mint),
|
|
// token.Transfer(srcATA, dstATA, payer, lamports)]
|
|
// 4. solana.NewTransaction(instructions, blockhash, WithPayer(payer))
|
|
// 5. Marshal the message portion to bytes — that's what gets signed.
|
|
//
|
|
// Errors propagated:
|
|
// - ErrSOLNoNetworkConfig network not registered.
|
|
// - solana.PublicKey parse errors malformed addresses.
|
|
// - rpc errors upstream RPC failure.
|
|
func (a *SOLAssembler) PreSign(ctx context.Context, in SOLSpec) (*SOLUnsigned, error) {
|
|
cfg, ok := a.Networks[in.Network]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrSOLNoNetworkConfig, in.Network)
|
|
}
|
|
if cfg.BlockhashURL == "" {
|
|
return nil, fmt.Errorf("txassembler: SOL network %s missing BlockhashURL", in.Network)
|
|
}
|
|
payer, err := solana.PublicKeyFromBase58(in.PayerAddress)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: invalid PayerAddress %q: %w", in.PayerAddress, err)
|
|
}
|
|
recipient, err := solana.PublicKeyFromBase58(in.RecipientAddress)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: invalid RecipientAddress %q: %w", in.RecipientAddress, err)
|
|
}
|
|
if in.LamportsAmount == 0 {
|
|
return nil, errors.New("txassembler: LamportsAmount must be > 0")
|
|
}
|
|
|
|
client := a.rpcFor(cfg.BlockhashURL)
|
|
bh, err := client.GetLatestBlockhash(ctx, cfg.Commitment)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: getLatestBlockhash: %w", err)
|
|
}
|
|
if bh == nil || bh.Value == nil {
|
|
return nil, errors.New("txassembler: getLatestBlockhash returned empty result")
|
|
}
|
|
blockhash := bh.Value.Blockhash
|
|
|
|
// Build the instruction list.
|
|
instructions, err := a.buildSOLInstructions(ctx, client, payer, recipient, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tx, err := solana.NewTransaction(
|
|
instructions,
|
|
blockhash,
|
|
solana.TransactionPayer(payer),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: NewTransaction: %w", err)
|
|
}
|
|
|
|
// MarshalBinary() on a Message produces the exact byte sequence that
|
|
// Solana signs — header + accountKeys + recentBlockhash + compiled
|
|
// instructions. The Transaction.MarshalBinary that includes
|
|
// signatures wraps these bytes with a [compactU16-prefixed] signature
|
|
// vector, but we don't have the signature yet, so we go straight to
|
|
// the message.
|
|
msgBytes, err := tx.Message.MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: marshal SOL message: %w", err)
|
|
}
|
|
|
|
return &SOLUnsigned{
|
|
Network: in.Network,
|
|
Blockhash: blockhash.String(),
|
|
MessageBytes: msgBytes,
|
|
tx: tx,
|
|
payer: payer,
|
|
}, nil
|
|
}
|
|
|
|
// buildSOLInstructions resolves native vs SPL mode and ATA creation.
|
|
func (a *SOLAssembler) buildSOLInstructions(
|
|
ctx context.Context,
|
|
client *rpc.Client,
|
|
payer, recipient solana.PublicKey,
|
|
in SOLSpec,
|
|
) ([]solana.Instruction, error) {
|
|
if in.SourceMint == "" {
|
|
// Native SOL transfer.
|
|
return []solana.Instruction{
|
|
system.NewTransferInstruction(in.LamportsAmount, payer, recipient).Build(),
|
|
}, nil
|
|
}
|
|
|
|
mint, err := solana.PublicKeyFromBase58(in.SourceMint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: invalid SourceMint %q: %w", in.SourceMint, err)
|
|
}
|
|
|
|
srcATA, _, err := solana.FindAssociatedTokenAddress(payer, mint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: source ATA derive: %w", err)
|
|
}
|
|
dstATA, _, err := solana.FindAssociatedTokenAddress(recipient, mint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: destination ATA derive: %w", err)
|
|
}
|
|
|
|
out := make([]solana.Instruction, 0, 2)
|
|
|
|
// Does the destination ATA already exist? Account-info nil ⇒ doesn't
|
|
// exist, prepend the ATA-create instruction. Any other error is
|
|
// transport / RPC failure — surface so the signing driver can retry.
|
|
exists, err := a.destATAExists(ctx, client, dstATA)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("txassembler: probe destination ATA: %w", err)
|
|
}
|
|
if !exists {
|
|
out = append(out, ata.NewCreateInstruction(payer, recipient, mint).Build())
|
|
}
|
|
|
|
out = append(out,
|
|
token.NewTransferInstruction(in.LamportsAmount, srcATA, dstATA, payer, nil).Build(),
|
|
)
|
|
return out, nil
|
|
}
|
|
|
|
// destATAExists checks whether the destination ATA already exists on
|
|
// chain by calling getAccountInfo. true ⇒ no need to create. false ⇒
|
|
// must prepend the ATA-create instruction.
|
|
func (a *SOLAssembler) destATAExists(ctx context.Context, client *rpc.Client, addr solana.PublicKey) (bool, error) {
|
|
res, err := client.GetAccountInfo(ctx, addr)
|
|
if err != nil {
|
|
// solana-go surfaces "account not found" as nil result + nil
|
|
// error in newer releases, but historically also as
|
|
// rpc.ErrNotFound. Handle both shapes.
|
|
if errors.Is(err, rpc.ErrNotFound) {
|
|
return false, nil
|
|
}
|
|
// "could not find account" appears in some error strings —
|
|
// fall through to the substring check so we don't false-flag
|
|
// transient RPC errors as missing accounts.
|
|
low := strings.ToLower(err.Error())
|
|
if strings.Contains(low, "could not find account") || strings.Contains(low, "account not found") {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
if res == nil || res.Value == nil {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Finalize
|
|
// =============================================================================
|
|
|
|
// SOLSignatureLen is the canonical Ed25519 signature size. Used by
|
|
// Finalize as a contract assertion before slotting the bytes in.
|
|
const SOLSignatureLen = 64
|
|
|
|
// Finalize takes the 64-byte Ed25519 signature from the MPC cluster
|
|
// and produces the wire-ready transaction as a base64 string.
|
|
//
|
|
// `sig` must be the raw 64-byte Ed25519 signature over MessageBytes.
|
|
// (Solana signatures are not "recoverable" — they're plain EdDSA with
|
|
// no v byte and no canonicalization.)
|
|
//
|
|
// The returned rawTxBase64 is ready to ship to JSON-RPC sendTransaction
|
|
// with `{encoding: "base64"}`. The returned signature is base58-encoded
|
|
// (Solana's canonical "tx hash" identifier).
|
|
func (a *SOLAssembler) Finalize(unsigned *SOLUnsigned, sig [SOLSignatureLen]byte) (rawTxBase64, signature string, err error) {
|
|
if unsigned == nil || unsigned.tx == nil {
|
|
return "", "", errors.New("txassembler: nil SOLUnsigned (call PreSign first)")
|
|
}
|
|
if len(unsigned.tx.Message.AccountKeys) == 0 {
|
|
return "", "", errors.New("txassembler: SOLUnsigned has empty AccountKeys (corrupt message)")
|
|
}
|
|
// Sanity: account[0] must be the payer (canonical Solana convention).
|
|
// solana.NewTransaction with TransactionPayer enforces this; reassert
|
|
// to catch any future regression.
|
|
if !unsigned.tx.Message.AccountKeys[0].Equals(unsigned.payer) {
|
|
return "", "", fmt.Errorf("txassembler: account[0] %s != payer %s",
|
|
unsigned.tx.Message.AccountKeys[0].String(),
|
|
unsigned.payer.String(),
|
|
)
|
|
}
|
|
|
|
var solSig solana.Signature
|
|
copy(solSig[:], sig[:])
|
|
|
|
// Solana wire requires len(signatures) == header.NumRequiredSignatures.
|
|
// For the bridge's release flow that's always 1 (just the payer).
|
|
unsigned.tx.Signatures = []solana.Signature{solSig}
|
|
|
|
raw, err := unsigned.tx.MarshalBinary()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("txassembler: marshal SOL tx: %w", err)
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(raw), solSig.String(), nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Signature byte conversion
|
|
// =============================================================================
|
|
|
|
// ParseSOLSignatureHex decodes a hex-encoded Ed25519 signature into a
|
|
// fixed-size 64-byte buffer. The dashboard returns Ed25519 signatures
|
|
// as 128 hex characters (64 bytes) in the "signature" JSON field;
|
|
// SignForWalletOnCurve doesn't post-process them, so the caller (the
|
|
// signing driver) parses them here.
|
|
//
|
|
// Accepts the input with or without a 0x prefix. Returns an error if
|
|
// the decoded length isn't exactly 64 bytes.
|
|
func ParseSOLSignatureHex(s string) ([SOLSignatureLen]byte, error) {
|
|
var out [SOLSignatureLen]byte
|
|
s = strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X")
|
|
if len(s) != SOLSignatureLen*2 {
|
|
return out, fmt.Errorf("txassembler: SOL signature must be %d bytes (%d hex chars), got %d hex chars",
|
|
SOLSignatureLen, SOLSignatureLen*2, len(s))
|
|
}
|
|
dec, err := hex.DecodeString(s)
|
|
if err != nil {
|
|
return out, fmt.Errorf("txassembler: decode SOL signature hex: %w", err)
|
|
}
|
|
copy(out[:], dec)
|
|
return out, nil
|
|
}
|
|
|
|
// LamportsFromFloat converts a human-readable SOL/SPL amount to lamports
|
|
// (uint64) safely. decimals is the asset's decimals — 9 for native
|
|
// SOL, varies for SPL tokens. Rounds half-away-from-zero on the last
|
|
// digit to avoid silently dropping sub-lamport precision.
|
|
//
|
|
// Returns ErrSOLAmountOverflow when the result exceeds uint64; the
|
|
// bridge's typical amounts are well under that, so an overflow means
|
|
// the caller passed a corrupt input.
|
|
var ErrSOLAmountOverflow = errors.New("txassembler: SOL amount overflows uint64")
|
|
|
|
func LamportsFromFloat(amount float64, decimals int) (uint64, error) {
|
|
if amount < 0 {
|
|
return 0, fmt.Errorf("txassembler: SOL amount must be non-negative; got %v", amount)
|
|
}
|
|
if amount == 0 {
|
|
return 0, nil
|
|
}
|
|
scale := math.Pow10(decimals)
|
|
scaled := amount * scale
|
|
if math.IsInf(scaled, 0) || math.IsNaN(scaled) || scaled > math.MaxUint64 {
|
|
return 0, ErrSOLAmountOverflow
|
|
}
|
|
// Banker-style rounding to whole lamports.
|
|
return uint64(math.Round(scaled)), nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helpers
|
|
// =============================================================================
|
|
|
|
// SOLDefaultBlockhashURL returns the default getLatestBlockhash URL for
|
|
// a known Solana network. Used by the bridge startup wiring when the
|
|
// operator hasn't supplied a custom URL via config / overrides.
|
|
func SOLDefaultBlockhashURL(network string) string {
|
|
switch network {
|
|
case "SOLANA_MAINNET":
|
|
return "https://api.mainnet-beta.solana.com"
|
|
case "SOLANA_DEVNET":
|
|
return "https://api.devnet.solana.com"
|
|
case "SOLANA_TESTNET":
|
|
return "https://api.testnet.solana.com"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|