Skip to main content

GalaChain core SDK

@gala-chain/gala-core-sdk is the layer every GalaChain product SDK and app shares: signing (native, personal-sign, EIP-712 and arbitrary typed data), the signers (private key, lazy private key, Gala Wallet, EIP-1193 browser wallets), the gateway transport (fetch requestor with timeouts, wire options, axios and ethers adapters), the gateway token reads (balances and holds, token classes, NFT collection authorizations) and writes (transfer, burn, lock, unlock), token class keys and write uniqueKeys, and the shared error model (GalaSDKError, its catalog, the failure view). It speaks to the GalaChain gateway generically; product backends (the DEX and Launchpad backends) have their own SDKs (@gala-chain/gswap-sdk, @gala-chain/launchpad-sdk) that build on it.

Install​

npm install @gala-chain/gala-core-sdk@next

1.0 is a release candidate, published under the next dist-tag: install @next, and pin the exact version in an app build. A product SDK takes core as a peer dependency: install it next to the product SDK, once.

Quick start​

import { PrivateKeySigner, serialize, signNative, normalizePrivateKey } from '@gala-chain/gala-core-sdk';

const signer = new PrivateKeySigner(process.env.GALACHAIN_PRIVATE_KEY!);
const signed = await signer.signObject('TransferToken', { quantity: '1', uniqueKey: 'k-1' });

// The primitives the signer is built on, byte-identical to @gala-chain/api:
const key = normalizePrivateKey(process.env.GALACHAIN_PRIVATE_KEY!);
console.log(serialize({ b: 1, a: 2 }), signNative(key, { a: 1 }));
key.fill(0);

What is in it​

AreaExports
SignersPrivateKeySigner, createLazyPrivateKeySigner, GalaWalletSigner, BrowserWalletSigner, signPreSend, type GalaChainSigner
Signingserialize, getPayloadToSign, signNative, signPersonalMessage, personalSignPrefixFor, fixpointPrefixFor, signChainDto*, signRawObject, generateEIP712Types, buildEIP712Payload, hashTypedData, signTypedDataPayload
TransportfetchRequestor, requestWithTimeout (kind: { write: false } makes a read's timeout and transport failure say write: false), buildRequestHeaders, buildQueryString, readRetryAfter, httpRequestorFromAxios, eip1193ProviderFromEthers, isFetchTransportFailure, types HttpRequestor, WireOptions, RequestKind
Gateway readsGatewayReads (fetchBalances, fetchTokenBalance, fetchNftBalances, fetchTokenClasses, fetchTokenClassesWithSupply, fetchTokenClass, fetchTokenDetails, fetchTokenClassesPage, fetchAllTokenClasses, fetchNftCollectionAuthorizationsPage, fetchAllNftCollectionAuthorizations, fetchPublicKey), GATEWAY_READ_METHODS, GATEWAY_READ_LIMITS, types GalaChainTokenBalance, GalaChainTokenBalanceSummary, GalaChainTokenClass, GalaChainTokenDetails, GalaChainPage, GalaChainPublicKey
Gateway writesGatewayWrites (transferGala, transferToken, burnTokens, lockTokens, unlockTokens), galaChainUserRef (a user reference in the one spelling the gateway accepts), GATEWAY_WRITE_METHODS, GATEWAY_WRITE_BATCH_LIMITS, GALA_TOKEN_CLASS_KEY, types GatewayWritesOptions, GatewayWriteReceipt, TokenNameResolver, TokenQuantityInput
DeploymentsGALA_CHAIN_ENVIRONMENTS (gateway and DEX API of prod / stage)
KeyscompositeKeyOf, tryParseTokenClassKey, tryParseCompositeKey, createUniqueKey, assertUniqueKey, isValidUniqueKey
WalletscreateWallet (random, from a private key, or from a 12/24-word mnemonic at m/44'/60'/0'/0/0), validateWalletInput, getPublicKeyFromPrivateKey, getEthereumAddressFromPrivateKey, checksumEthereumAddress, types GeneratedWallet, PublicKeyResult
ErrorsGalaSDKError, isGalaError, GALA_ERROR_CATALOG, galaErrorTemplates, GALA_ERROR_PHASE_BY_CODE, toFailureView, isWriteView, FAILURE_OUTCOMES
Wallet helpers (./wallet)isWalletUserRejection, walletDeclineCode, isGalaErrorLike
Wallet providers (./providers)PrivateKeyProvider, ExternalWalletProvider, GalaWalletProvider, detectWallets, subscribeToWalletChanges, getWalletByRdns, isWalletAvailable, findMetaMaskWallet, getMetaMaskWallet, findTrustWallet, getTrustWallet, hasAnyWallet, getInjectedEip1193Provider, KNOWN_WALLET_RDNS, createWalletProvider, createProviderFromEip1193, ensureConnected, getPrivateKeyIfAvailable, isPrivateKeyProvider, isExternalProvider, isEip1193Provider, type WalletProvider
React (./react)WalletProvider (context component), WalletContext, useWallet, useWalletContext, useWalletConnection, useWalletDetection, useWalletAddress, useGalaAddress, useWalletProvider, useIsWalletConnected
Bridge (./bridge)BridgeService (bridge out to and in from Ethereum and Solana, fees, status, external balances), EthereumBridgeStrategy, SolanaBridgeStrategy, BaseBridgeStrategy, GalaConnectClient, BridgeableTokenService, WrappableTokenService, WrapService (channel wrap / unwrap), createSolanaWallet, buildBridgeOutPayload (the signed bridge-out request), the bridge constants and token tables, the bridge types, RateLimiter, the token amount helpers (toBaseUnits, fromBaseUnits, formatTokenAmount, compareAmounts)

Subpaths​

Each subpath is its own build: it loads the root (and ./react the providers) through the package, never a copy, so an app has one GalaSDKError class, one signer set and one set of provider classes (checked on the packed tarball by pnpm run check:pack, and on the source by tests/subpath_isolation.test.ts). The root never loads a subpath.

EntryWhat it is forLoads (besides the root)Peer it needs
@gala-chain/gala-core-sdkSigning, signers, transport, gateway reads and writes, keys, wallets, errors@noble/*, @scure/bip32, @scure/bip39, bignumber.jsnone
@gala-chain/gala-core-sdk/walletThe dependency-free wallet helpers (an app shell)nothing (not even the root)none
@gala-chain/gala-core-sdk/providersOne WalletProvider per wallet kind, EIP-6963 detection@noble/hashesnone
@gala-chain/gala-core-sdk/reactThe wallet context and hooks./providersreact (optional peer, 16.8 to 19; react-dom is not needed)
@gala-chain/gala-core-sdk/bridgeBridging GalaChain to and from Ethereum and Solana, channel wrap / unwrap, the bridgeable and wrappable token listsethers, @solana/web3.js, @solana/spl-token, @scure/base, bignumber.jsethers 6, @solana/web3.js 1.99+, @solana/spl-token 0.4.15+ (optional peers: install them when you import ./bridge)

Only ./bridge loads ethers or @solana/*: the root and the other subpaths never do (checked on the source by pnpm run check:rn-safety and the isolation test, and on the built files by the packed smoke).

Bridge​

import { BridgeableTokenService, BridgeService, GalaConnectClient } from '@gala-chain/gala-core-sdk/bridge';

const walletAddress = 'eth|...';
// The DEX API's bridgeable token list (production by default; `baseUrl` for another deployment)
const tokens = new BridgeableTokenService(new GalaConnectClient({ walletAddress }));
const bridge = new BridgeService({
environment: 'prod', // or 'stage': GalaConnect, the gateway and the RPCs of that deployment
galaChainWalletAddress: walletAddress,
ethereumPrivateKey: process.env.ETH_PRIVATE_KEY!, // signs the bridge-out request and Ethereum deposits
bridgeableTokenService: tokens, // resolves a token id to its bridge symbol
});
// Token ids are class keys (pipe form or object): a bare symbol is refused (symbols are not unique)
const GALA = 'GALA|Unit|none|none';
const fee = await bridge.estimateBridgeFee({ tokenId: GALA, destinationChain: 'Ethereum' });
const out = await bridge.bridgeOut({ tokenId: GALA, amount: '10', destinationChain: 'Ethereum', recipientAddress: '0x...' });
const status = await bridge.waitForBridgeCompletion(out.transactionHash);

A bridge or wrap that does not complete throws a GalaSDKError (never a "failed" result). Once a bridge-out request was accepted the tokens may be locked: a later failure is BRIDGE_OUT_OUTCOME_UNKNOWN (with the step, the bridge request id and the uniqueKey), and a deposit that may have been sent is BRIDGE_DEPOSIT_OUTCOME_UNKNOWN; neither is ever retried by the SDK. WrapService wraps a token from its own channel to the asset channel (MUSIC to GMUSIC) and back, signed natively by a GalaChainSigner.

TypeScript: the ./bridge declarations name @solana/web3.js types, whose own declarations import Node's http and https, so a TypeScript app that imports ./bridge has @types/node (with TypeScript 6, "types": ["node"]) or skipLibCheck. The other entries need no Node types.

Wallet providers​

import { detectWallets, ExternalWalletProvider, PrivateKeyProvider } from '@gala-chain/gala-core-sdk/providers';

const { wallets } = await detectWallets(); // EIP-6963 announcements, then window.ethereum / window.gala
const first = wallets[0];
if (first === undefined) throw new Error('No wallet announced itself');
const provider = new ExternalWalletProvider(first.provider);
await provider.connect();
await provider.signMessage('Hello'); // personal_sign
const signer = provider.getSigner(); // a GalaChainSigner for GatewayWrites, gswap-sdk, ...

const server = new PrivateKeyProvider(process.env.GALACHAIN_PRIVATE_KEY!); // Node, CLI, bots

A provider listens to its wallet only while connected, follows accountsChanged (an empty or malformed account list disconnects it), and reads its connection once per operation, so one operation never names two accounts. getSigner() is bound to the account connected when it is called: after an account change, get a new signer. GalaWalletProvider selects its account in the extension before each signature, serialized with every other signer of the same extension. Every failure is a GalaSDKError: WALLET_CONNECTION_FAILED (user_rejected, no_accounts, failed), WALLET_NOT_CONNECTED, SIGNING_FAILED (user_rejected when the person declined), INVALID_SIGNATURE, PRIVATE_KEY_NOT_ACCESSIBLE, INVALID_WALLET_PROVIDER_CONFIG. Detection works without a window (Node, SSR, React Native): it finds nothing and throws nothing.

React​

import { WalletProvider, useWalletConnection, useWalletDetection, useWallet } from '@gala-chain/gala-core-sdk/react';

<WalletProvider autoDetect>
<App />
</WalletProvider>;

const { wallets } = useWalletDetection();
const { connectDetected } = useWalletConnection(); // a GalaWalletProvider for Gala Wallet, an ExternalWalletProvider otherwise
const { address, galaAddress, isConnected } = useWallet();

The address follows the wallet (account switches, a wallet-side disconnect); the latest connect wins over a slower earlier one; a provider the context created is released when it is replaced, when its connect fails and on unmount; initialProvider is connected once. A hook outside <WalletProvider> is WALLET_CONTEXT_MISSING. It renders on the server (no window, nothing detected). ./react does not re-export the providers: import them from ./providers.

Every error core throws is a GalaSDKError with a stable code, flat params for an app's own localization and a phase (pre_send: nothing was transmitted). The codes are listed in docs/error-catalog.md. A product SDK's error class extends GalaSDKError and adds its own codes; isGalaError(error) recognises an error from any of them.

Gateway writes​

import { GatewayWrites, PrivateKeySigner } from '@gala-chain/gala-core-sdk';

const writes = new GatewayWrites({
gatewayBaseUrl: 'https://gateway-mainnet.galachain.com',
signer: new PrivateKeySigner(process.env.GALACHAIN_PRIVATE_KEY!),
walletAddress: 'eth|70997970C51812dc3A010C7d01b50e0d17dc79C8', // or a client| alias
});

const { transactionId, blockNumber } = await writes.transferGala({ to: 'client|abc', quantity: '1.5' });
await writes.lockTokens({ tokens: [{ tokenClassKey: 'GALA|Unit|none|none', quantity: '10' }], expires: 0 });
  • Quantities are WHOLE tokens, as the decimal string the chain receives; they are signed exactly as written ("1.50" stays "1.50") and never scaled.
  • Tokens: tokenClassKey (collection|category|type|additionalKey with exactly four parts, the $ composite key, or the object) plus instance (default "0"), or tokenName through an optional tokenNameResolver. A bare symbol is never a class key.
  • Identities (to, walletAddress, lockAuthority): an Ethereum account (eth|<40 hex>, 0x<40 hex>, bare hex; any casing but a wrong EIP-55 checksum) is always sent as eth|<checksummed>, the only spelling the gateway accepts; client|<id> is sent as written. eth|0x<hex> is not an address form: it is refused before anything is signed or sent with INVALID_ETH_USER_REF_PREFIX (an eth| reference takes the 40 digits with no 0x; the error's expected is the right spelling). A bare 0x<40 hex> is an ordinary Ethereum address and is converted.
  • The signer is handed the bare DTO; the body sent is exactly the DTO plus the signature (a signer that adds or changes a field is refused). A uniqueKey is minted per write unless you pass one.
  • GalaChain's Status is checked before anything else is trusted; a timeout or broken connection is GATEWAY_WRITE_OUTCOME_UNKNOWN: the write may have been applied, so check its uniqueKey and never resend it under a fresh key.

Gateway reads​

import { GALA_CHAIN_ENVIRONMENTS, GatewayReads } from '@gala-chain/gala-core-sdk';

const reads = new GatewayReads({ gatewayBaseUrl: GALA_CHAIN_ENVIRONMENTS.prod.gatewayBaseUrl });

const gala = await reads.fetchTokenBalance({ owner: 'client|abc', tokenClassKey: 'GALA|Unit|none|none' });
console.log(gala?.quantity, gala?.lockedQuantity, gala?.availableQuantity); // whole-token strings

const { symbol, decimals } = await reads.fetchTokenDetails('GUSDC|Unit|none|none');
const classes = await reads.fetchAllTokenClasses({ collection: 'BONKTactics' }, { maxPages: 10 });
  • Reads are unsigned and apply nothing: every failure after the request left says so (error.write === false), and a retry is always safe.
  • Owners are spelled as the writes spell them (an Ethereum account in any form as eth|<checksummed>, client| as written); class keys are accepted in the same forms. $MUSIC balances are read on the gateway's music channel.
  • Every answer is checked (GalaChain's Status, then each row) before it is returned; a balance of another owner than the one asked for is refused. Quantities stay the chain's whole-token decimal strings; hold totals are exact and never in exponent form.
  • A token class that does not exist is TOKEN_CLASS_NOT_FOUND; a fetchAll... read that would need more than maxPages pages is PAGINATION_LIMIT_EXCEEDED (never a truncated list).
  • fetchPublicKey(user) reads the key GalaChain has registered for a user ({ publicKey, signing? }, undefined when none is saved). The public gateways do not route the public-key-contract; read it through a connect-api proxy: new GatewayReads({ gatewayBaseUrl: 'https://api-galaswap.gala.com/galachain' }).

Signing parity​

The signing code is pinned byte-for-byte to @gala-chain/api and to the Gala Wallet's and the gSwap web app's signing code by the golden vectors in tests/fixtures/wallet-signing-golden.json and tests/fixtures/frontend-signing-golden.json (tests/signing-golden.test.ts). They are never regenerated to make a test pass.

React Native​

The shipped source uses no Node builtin, no Buffer, no process, and never loads ethers, axios or @gala-chain/api (pnpm run check:rn-safety, also run by the test suite). The only polyfill React Native/Hermes needs is crypto.getRandomValues, for minting uniqueKeys and new random wallets (createWallet()):

import 'react-native-get-random-values'; // once, before the SDK mints a key

Without it, minting a key or a wallet throws CSPRNG_UNAVAILABLE (never a weaker fallback).

./bridge is the exception: it loads ethers, @solana/web3.js and @solana/spl-token, and an app that imports it on React Native also installs what those libraries ask for there (crypto.getRandomValues above, which createSolanaWallet needs too; @solana/web3.js's React Native guidance adds a global Buffer from the buffer package). Core's own ./bridge code uses no Buffer or process global (Solana instruction data are Uint8Arrays) and touches none at module load: the guard allows ./bridge exactly those three packages and nothing else (base58 is @scure/base, pure JS like the rest of core).

Bundle size​

sideEffects: false, and the browser build is one file per source module, so an app (or a product SDK's light module) that imports one helper loads that module only. @gala-chain/gala-core-sdk/wallet holds only the dependency-free wallet helpers (about 1.2 kB minified, capped at 2 kB). tests/tree_shaking.test.ts budgets every shell-safe helper. The browser builds of ./providers (about 17 kB minified, 7.6 kB gzipped, most of it the known wallets' icons), ./react (about 4 kB, 1.7 kB gzipped) and ./bridge (about 60 kB minified, 16 kB gzipped, before ethers and @solana/*) are loaded only by an app that imports them.

Status​

.0.0-rc.8 (rc.7: eth|0x<hex> is refused as INVALID_ETH_USER_REF_PREFIX, not an address form; rc.6: fetchPublicKey, MetaMask and Trust Wallet detection, silent restore(); rc.5: every Ethereum account a read, write or GalaConnect client sends is eth|<EIP-55>, the only spelling the gateway accepts, and galaChainUserRef is exported; rc.4: bridgeableTokenService is required). In: signing, signers, transport and the error model (rc.0, from gswap-sdk); the gateway token writes (rc.1, from launchpad-sdk's transfer and lock services, which now delegate here); the gateway token reads (rc.2: balances, token classes, NFT collection authorizations, from launchpad-sdk's balance and token services, which now delegate here) and the deployment endpoints; the wallet providers (./providers), React hooks (./react), wallet helpers (createWallet and the key derivations) and the bridge (./bridge: bridging, the token lists, wrap / unwrap, createSolanaWallet) (rc.3, from launchpad-sdk's /wallet, /react, helpers/wallet, bridge/ and its bridgeable / wrappable token and wrap services, which are gone).

Migrating from gswap-sdk 1.x​

See MIGRATING-FROM-GSWAP.md: every moved export, old path to new path, and the renamed error and brand identifiers.

License​

Apache License 2.0.