A Go SDK for building, signing, and sending transactions on the
Arch Network. It is a faithful port of the
arch-typescript-sdk
and the Rust arch_sdk, byte-for-byte compatible with the node's wire
formats: message serialization and hashing are pinned against fixtures
generated by the TypeScript SDK, the JSON-RPC request/response shapes against
the node's own wire-format tests, and BIP-322 signing against the test vector
of the exact bip322 crate the node uses.
go get github.com/Arch-Network/arch-goArch's programming model is Solana-derived (accounts, instructions, compiled messages), but the wire is not Solana-compatible. The differences this SDK handles for you:
- RPC: JSON-RPC 2.0 with Arch method names (
read_account_info,get_best_block_hash, ...) and byte arrays as JSON number arrays — not base64/base58 blobs, and no commitment levels. - Serialization: message binary layout uses u32 little-endian counts, not compact-u16 ("shortvec").
- Signing: keys are secp256k1 x-only (Bitcoin Taproot), not ed25519. The
signing digest is the 64 ASCII bytes of
hex(sha256(hex(sha256(serialize(message))))), signed with BIP-322 (taproot key-spend,SIGHASH_ALL), transported as the first 64 bytes of the witness signature. - Addresses: an account's on-chain address is a taproot output controlled
by the network's distributed signing key, not by the account key itself.
Derive it offline with
AccountAddress(networkKey, accountKey, params)(network key fromClient.GetNetworkPubkey+NetworkPubkeyFromHex), or ask the node withClient.GetAccountAddress.P2TRAddressis the plain taproot address of a key — what BIP-322 signatures commit to — and is not the account address.
package main
import (
"fmt"
"log"
arch "github.com/Arch-Network/arch-go"
)
func main() {
client := arch.NewClient("http://localhost:9002", nil)
// Your key. Any *btcec.PrivateKey works; wallets that already handle
// Bitcoin Taproot keys can reuse them directly.
priv, err := arch.NewPrivateKey()
if err != nil {
log.Fatal(err)
}
payer := arch.XOnlyPubkey(priv)
var to arch.Pubkey // recipient account pubkey
instruction := arch.Transfer(payer, to, 1_000)
blockhashHex, err := client.GetBestBlockHash()
if err != nil {
log.Fatal(err)
}
blockhash, err := arch.HashFromHex(blockhashHex)
if err != nil {
log.Fatal(err)
}
message, err := arch.NewSanitizedMessage([]arch.Instruction{instruction}, &payer, blockhash)
if err != nil {
log.Fatal(err)
}
tx, err := arch.BuildAndSignTransaction(message, priv)
if err != nil {
log.Fatal(err)
}
txid, err := client.SendTransaction(tx)
if err != nil {
log.Fatal(err)
}
fmt.Println("txid:", txid)
}If the wallet signs BIP-322 itself, build the message, hand it the digest, and normalize the returned signature:
digest := message.Hash() // 64 ASCII bytes; sign these with BIP-322
// walletSig is 64, 66, or 67 bytes depending on the wallet's encoding.
sig, err := arch.AdjustSignature(walletSig)
tx := arch.RuntimeTransaction{
Version: arch.RuntimeTxVersion,
Signatures: []arch.Signature{sig},
Message: message,
}You can verify any signature locally with arch.VerifyMessageBIP322.
Client covers the node's full JSON-RPC surface:
| Method | RPC |
|---|---|
SendTransaction / SendTransactions |
send_transaction / send_transactions |
ReadAccountInfo / GetMultipleAccounts |
read_account_info / get_multiple_accounts |
GetAccountAddress |
get_account_address |
GetBestBlockHash / GetBestFinalizedBlockHash |
get_best_block_hash / get_best_finalized_block_hash |
GetBlock / GetBlockByHeight / GetFullBlockByHash / GetFullBlockByHeight |
get_block / get_block_by_height |
GetBlockCount / GetBlockHash |
get_block_count / get_block_hash |
GetProcessedTransaction / GetTransactionStatus |
get_processed_transaction / get_transaction_status |
GetTransactionsByBlock / GetTransactionsByIds / RecentTransactions |
get_transactions_by_block / get_transactions_by_ids / recent_transactions |
GetProgramAccounts |
get_program_accounts (with DataSizeFilter / DataContentFilter) |
RequestAirdrop / CreateAccountWithFaucet |
request_airdrop / create_account_with_faucet (regtest/testnet) |
GetNetworkPubkey / CheckPreAnchorConflict |
get_network_pubkey / check_pre_anchor_conflict |
CreateNewAccount |
helper: fresh keypair + node-derived address |
Lookups return nil (not an error) when the node reports not-found; all other
node errors surface as *arch.RPCError with the node's code and message.
Builders for every system instruction: CreateAccount,
CreateAccountWithAnchor (anchor an account to a Bitcoin UTXO), Assign,
Anchor, SignInput, Transfer, Allocate, CreateAccountWithSeed,
AllocateWithSeed, AssignWithSeed, TransferWithSeed. Program-ID
constants for the token, associated-token, BPF-loader, vote, stake,
compute-budget, and native-loader programs are in constants.go.
Builders for the APL token program (an SPL Token fork) and the associated
token account program: TokenInitializeMint(2), TokenInitializeAccount
(2/3), TokenInitializeMultisig, TokenTransfer, TokenApprove,
TokenRevoke, TokenSetAuthority, TokenMintTo, TokenBurn,
TokenCloseAccount, TokenFreezeAccount, TokenThawAccount, the *Checked
variants, TokenSyncNative, and CreateAssociatedTokenAccount
(WithAnchor). Pass nil multisig signers for a single-key authority, or the
signer set when the authority is a multisig account.
ata, _, err := arch.AssociatedTokenAddress(wallet, mint)
ix := arch.TokenTransfer(sourceATA, destATA, owner, nil, 1_000_000)Program-derived addresses use FindProgramAddress / CreateProgramAddress.
Note Arch's derivation differs from Solana's — the hash is
sha256(seeds || bump || program_id) with no domain-separator string — so
Solana PDA code will produce wrong addresses here.
Builders for deploying programs: LoaderWrite, LoaderTruncate,
LoaderDeploy, LoaderRetract, LoaderTransferAuthority, LoaderFinalize,
and ExtendBytesMaxLen (the largest LoaderWrite payload that fits in one
transaction, for chunking ELF uploads).
The validator pushes events over a plain WebSocket endpoint (default port
10081). Topics: block, transaction, account_update,
rolledback_transactions, reapplied_transactions, dkg, with optional
server-side field filters:
ws, err := arch.DialWebSocket(ctx, "ws://localhost:10081")
if err != nil {
log.Fatal(err)
}
defer ws.Close()
// nil filter receives every event on the topic; a filter matches event
// fields exactly (array values match if the event's array contains them).
sub, err := ws.Subscribe(ctx, arch.TopicTransaction, arch.EventFilter{
"status": map[string]any{"type": "processed"},
})
if err != nil {
log.Fatal(err)
}
for ev := range sub.Events() {
txEv, err := ev.Transaction()
if err != nil {
log.Fatal(err)
}
fmt.Println("processed:", txEv.Hash, "at height", txEv.BlockHeight)
}Subscription.Unsubscribe cancels one subscription; closing the client closes
every subscription's event channel.
go test ./...The suite pins:
- message serialization and the signing digest against fixtures generated by the TypeScript SDK;
- JSON request envelopes, parameter shapes, and response decoding against the
node's
rpc_wire_format.rsfixtures, via a mock HTTP node; - BIP-322 signing against the rust
bip322crate's taproot test vector, plus sign/verify round trips; - key compilation (payer first, then writable signers, readonly signers,
writable non-signers, readonly non-signers, byte-sorted within groups)
against the Rust
compiled_keyssemantics; - system, token, and loader instruction data layouts byte-for-byte;
- PDA derivation (including the associated token address) against addresses
generated by running the Rust
arch_programderivation; - the WebSocket client against a mock server speaking the validator's exact subscription wire format;
- account-address derivation against an address captured live from the testnet node.
A live integration test runs the whole stack — reads, offline address derivation, the faucet flow, and SDK-built, BIP-322-signed transactions — against a real network:
ARCH_RPC_URL=https://rpc.testnet.arch.network go test -run TestLive -v -timeout 600sNote the faucet flow: CreateAccountWithFaucet returns a transaction signed
by the faucet that the created account must co-sign (append
SignMessageBIP322(priv, tx.Message.Hash()) to tx.Signatures) before
sending.
MIT