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
| Area | Exports |
|---|---|
| Signers | PrivateKeySigner, createLazyPrivateKeySigner, GalaWalletSigner, BrowserWalletSigner, signPreSend, type GalaChainSigner |
| Signing | serialize, getPayloadToSign, signNative, signPersonalMessage, personalSignPrefixFor, fixpointPrefixFor, signChainDto*, signRawObject, generateEIP712Types, buildEIP712Payload, hashTypedData, signTypedDataPayload |
| Transport | fetchRequestor, 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 reads | GatewayReads (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 writes | GatewayWrites (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 |
| Deployments | GALA_CHAIN_ENVIRONMENTS (gateway and DEX API of prod / stage) |
| Keys | compositeKeyOf, tryParseTokenClassKey, tryParseCompositeKey, createUniqueKey, assertUniqueKey, isValidUniqueKey |
| Wallets | createWallet (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 |
| Errors | GalaSDKError, 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.
| Entry | What it is for | Loads (besides the root) | Peer it needs |
|---|---|---|---|
@gala-chain/gala-core-sdk | Signing, signers, transport, gateway reads and writes, keys, wallets, errors | @noble/*, @scure/bip32, @scure/bip39, bignumber.js | none |
@gala-chain/gala-core-sdk/wallet | The dependency-free wallet helpers (an app shell) | nothing (not even the root) | none |
@gala-chain/gala-core-sdk/providers | One WalletProvider per wallet kind, EIP-6963 detection | @noble/hashes | none |
@gala-chain/gala-core-sdk/react | The wallet context and hooks | ./providers | react (optional peer, 16.8 to 19; react-dom is not needed) |
@gala-chain/gala-core-sdk/bridge | Bridging GalaChain to and from Ethereum and Solana, channel wrap / unwrap, the bridgeable and wrappable token lists | ethers, @solana/web3.js, @solana/spl-token, @scure/base, bignumber.js | ethers 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|additionalKeywith exactly four parts, the$composite key, or the object) plusinstance(default"0"), ortokenNamethrough an optionaltokenNameResolver. 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 aseth|<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 withINVALID_ETH_USER_REF_PREFIX(aneth|reference takes the 40 digits with no0x; the error'sexpectedis the right spelling). A bare0x<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
uniqueKeyis minted per write unless you pass one. - GalaChain's
Statusis checked before anything else is trusted; a timeout or broken connection isGATEWAY_WRITE_OUTCOME_UNKNOWN: the write may have been applied, so check itsuniqueKeyand 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.$MUSICbalances are read on the gateway'smusicchannel. - 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; afetchAll...read that would need more thanmaxPagespages isPAGINATION_LIMIT_EXCEEDED(never a truncated list). fetchPublicKey(user)reads the key GalaChain has registered for a user ({ publicKey, signing? },undefinedwhen 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.