xrpl-gaming-core
v0.2.0
Published
Core of the Kinesis XRPL DynamicNFT Gaming SDK. Provides the XRPLGamingSDK class plus pluggable IDBAdapter and IIPFSAdapter interfaces for minting, updating, transferring, burning and querying mutable game NFTs on the XRP Ledger.
Downloads
251
Maintainers
Readme
xrpl-gaming-core
The core of the XRPL Gaming SDK. Provides the XRPLGamingSDK class and the pluggable adapter interfaces (IDBAdapter, IIPFSAdapter) that power it.
Install
pnpm add xrpl-gaming-core xrpl-gaming-ipfs-pinata xrpl-gaming-db-postgresSelf-hosted usage
You bring your own XRPL wallet seed, XRPL node URL, database, and Pinata account. The SDK glues them together.
import { XRPLGamingSDK } from "xrpl-gaming-core";
import { PinataAdapter } from "xrpl-gaming-ipfs-pinata";
import { PostgresAdapter } from "xrpl-gaming-db-postgres";
const sdk = new XRPLGamingSDK({
xrpl: {
nodeUrl: "wss://xrplcluster.com",
issuerWallet: { seed: process.env.XRPL_ISSUER_SEED! },
},
db: new PostgresAdapter({ connectionString: process.env.DATABASE_URL! }),
ipfs: new PinataAdapter({ jwt: process.env.PINATA_JWT! }),
});
await sdk.init();
// Mint a DynamicNFT for a player
const { record, txHash, offerId } = await sdk.nft.mint({
metadata: { name: "Wandering Knight", class: "warrior", level: 1, power: 50 },
playerId: "player-123",
collection: "characters",
destination: "rPlayerWalletAddress...", // optional sell offer
});
// Later, level up — updates the on-chain URI via NFTokenModify
await sdk.nft.update(record.tokenId, {
metadata: { name: "Wandering Knight", class: "warrior", level: 2, power: 95 },
});
// Read current state from your DB
const current = await sdk.nft.get(record.tokenId);
// Transfer to another wallet (creates a sell offer — destination must accept)
const { offerId } = await sdk.nft.transfer(record.tokenId, {
destination: "rNewOwner...",
});
// `record.ownerAddress` does NOT change yet — XRPL ownership only flips
// when the destination calls NFTokenAcceptOffer. The DB row gets
// `pendingOfferId` and `pendingDestination` annotations so you can show
// "transfer pending" in your UI.
//
// Once you've confirmed the offer was accepted (e.g. by polling
// `account_nfts` or subscribing to ledger events), reconcile the DB:
await sdk.nft.markTransferComplete(record.tokenId, "rNewOwner...");
// Burn (issuer-owned only)
await sdk.nft.burn(record.tokenId);
await sdk.close();Managed usage (preview)
The managed tier is not yet available. Constructing the SDK with managedApiKey throws a ManagedNotAvailableError with instructions to use the self-hosted path or get in touch.
import { XRPLGamingSDK } from "xrpl-gaming-core";
// Will throw ManagedNotAvailableError until the hosted backend is online.
const sdk = new XRPLGamingSDK({ managedApiKey: "xg_live_xxx" });DynamicNFT details
Mint flags
mint() exposes the four NFTokenMint flags that matter for game NFTs. All are optional booleans on MintParams:
| MintParams field | XRPL flag | Default | Effect |
| ------------------ | ---------------- | ------- | ----------------------------------------------------------------------- |
| transferable | tfTransferable | true | NFT can be traded between non-issuer wallets. |
| mutable | tfMutable | true | Issuer can later call update() (XLS-46). false freezes the NFT. |
| burnable | tfBurnable | false | Issuer can burn the NFT even after a player owns it. |
| onlyXRP | tfOnlyXRP | false | Sell/buy offers for the NFT must be denominated in XRP, not IOUs. |
Updating a player-held NFT
update() issues an NFTokenModify transaction that replaces the URI with a new IPFS pointer. XLS-46 requires this transaction to carry an Owner field whenever the issuer is acting on a token they no longer hold (typical once a player has accepted the sell offer). The SDK attaches Owner automatically by resolving the current holder from one of two sources, controlled by UpdateParams.ownerSource:
"onchain"(default) — the SDK queries the XRPL via the Clio-onlynft_infoRPC. Always correct, costs one extra request per update. Requires the SDK'snodeUrlto point at a Clio server. If you self-host a rippled-only node and the call fails, the SDK throws anXrplGamingErrorthat explicitly tells you to either run Clio or switch to"db"."db"— the SDK trustsownerAddresson its DB record. Skips the network round-trip, but only safe if you reliably callsdk.nft.markTransferComplete(tokenId, newOwner)after every accepted sell offer. Otherwise the modify will be rejected withtecNO_ENTRY/tecNO_PERMISSION.
// Default — works against any Clio-fronted XRPL node:
await sdk.nft.update(tokenId, { metadata: { ...newAttrs } });
// Opt out of the network call — your app guarantees DB freshness:
await sdk.nft.update(tokenId, { metadata: { ...newAttrs }, ownerSource: "db" });Mint with an inline sell offer (XLS-46)
When you pass destination to mint(), the SDK does not issue a follow-up NFTokenCreateOffer. Instead it packs the XLS-46 inline sell-offer fields (Amount, Destination, Expiration) onto the same NFTokenMint transaction, so mint and offer settle in a single ledger close.
await sdk.nft.mint({
metadata,
destination: "rPlayerWalletAddress...",
amount: "0", // drops; default "0" = free claim
expiration: new Date(Date.now() + 24 * 60 * 60 * 1000), // optional Date OR Ripple-time number
});amount is XRP drops as a string ("0" = free claim, "1000000" = 1 XRP, etc.). expiration accepts either a JS Date or a Ripple-time number (seconds since 2000-01-01 UTC); the SDK calls toRippleTime() for you. Both fields are only meaningful when destination is set — passing them without a destination throws an XrplGamingError, since they describe the inline sell offer rather than the NFT itself. The returned result.offerId is pulled out of the same transaction's metadata, so you get both the new tokenId and the new offer id from a single round-trip — one signed transaction, one fee, and no possibility of an NFT existing on-ledger without its corresponding offer.
Transfer
Transfers use NFTokenCreateOffer (sell offer at 0 drops by default) targeted to the destination wallet, which must accept the offer to complete the transfer. The SDK does not poll the ledger; reconcile via markTransferComplete() once you observe acceptance.
IPFS adapter capabilities
IIPFSAdapter exposes two methods:
uploadJson(metadata, opts?)— pin a JSON metadata document. Used bynft.mint/nft.updateinternally; you rarely call it directly.uploadFile(data, opts?)— pin a binary file (image, audio, video). Use it to pin an NFT's image first, embed the returnedipfs://URI in the metadata, then mint:import { promises as fs } from "node:fs"; const bytes = await fs.readFile("./hero.png"); const image = await sdk.ipfs.uploadFile(bytes, { name: "hero.png", contentType: "image/png", }); await sdk.nft.mint({ metadata: { name: "Hero", image: image.uri, attributes: { level: 1 } }, playerId: "player-123", });dataacceptsUint8Array,ArrayBuffer,Blob, or a NodeBuffer. PasscontentTypeso the asset is served with the right MIME type. Both bundled adapters (xrpl-gaming-ipfs-pinataand any custom one you implement) support both methods.
Operation ordering & consistency model
mint() and update() perform their XRPL transaction first and then write/patch the DB record:
ipfs.uploadJson(metadata)— pin metadataclient.submitAndWait(NFTokenMint | NFTokenModify)— settle on-chaindb.saveNft(...)/db.updateNft(...)— persist the authoritative on-chain result (token id, URI, timestamps)
This guarantees we never advertise a DB row that points at a non-existent or out-of-date ledger object. The trade-off is that if the DB write fails after a successful XRPL settlement, you can rebuild the row by reading account_nfts for the issuer wallet and replaying it through db.saveNft. For mint({ destination }) the mint and the inline sell offer settle in one transaction (XLS-46), so the SDK saves a single DB row with pendingOfferId and pendingDestination already populated — there is no intermediate state in which an NFT can exist on-ledger without its offer.
Transfer is offer-based — you must reconcile
sdk.nft.transfer() does not flip ownership immediately. It issues an NFTokenCreateOffer (sell offer) targeted at the destination wallet and returns the offerId. Until the destination wallet calls NFTokenAcceptOffer on the XRPL, the issuer still owns the token. The DB row reflects this with two columns:
pendingOfferId— the open sell offer idpendingDestination— the intended recipient
Your application is responsible for detecting acceptance (poll account_nfts for the destination, or subscribe to ledger events) and then calling sdk.nft.markTransferComplete(tokenId, newOwnerAddress) to clear the pending fields and update ownerAddress. Future managed-tier and server packages will provide a watcher that automates this.
Build your own adapter
Implement IDBAdapter or IIPFSAdapter for any backend you like. See xrpl-gaming-db-mongodb and xrpl-gaming-ipfs-pinata for reference implementations.
