Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ scarb build
snforge test
```

## 🧰 SDK

The TypeScript SDK can fetch Beast data from Summit API, fall back to Voyager, and finally to RPC using the onchain data provider. It also renders the NFT card locally.

```ts
import { createBeastSdk, generateBeastSvg, toBeastSvgInput } from "@lootsurvivor/beast-sdk";

const sdk = createBeastSdk({
apiBaseUrl: "https://your-summit-api.example",
voyager: {
apiKey: "YOUR_VOYAGER_API_KEY",
},
// RPC defaults to https://api.cartridge.gg/x/starknet/mainnet/rpc/v0_10
// and the mainnet Beast data provider.
});

const { beasts } = await sdk.getAccountBeasts("0x0123...");
const svg = beasts[0] ? generateBeastSvg(toBeastSvgInput(beasts[0])) : null;
```

## 🏗️ Architecture

### Smart Contract Structure
Expand Down
2 changes: 2 additions & 0 deletions sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
59 changes: 59 additions & 0 deletions sdk/__tests__/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import {
normalizeStarknetAddress,
resolveBeastRenderData,
toBeastSvgInput,
} from "../client";
import type { BeastOnchain } from "../types";

describe("client helpers", () => {
it("normalizes Starknet addresses", () => {
const normalized = normalizeStarknetAddress("0xabc");
expect(normalized).toBe(`0x${"0".repeat(61)}abc`);
});

it("resolves render data from on-chain fields", () => {
const beast: BeastOnchain = {
tokenId: 77,
beastId: 29,
prefix: 2,
suffix: 1,
level: 4,
health: 10,
shiny: 1,
animated: 0,
rank: 2,
};

const resolved = resolveBeastRenderData(beast);

expect(resolved.name).toBe("Dragon");
expect(resolved.prefixName).toBe("Apocalypse");
expect(resolved.suffixName).toBe("Bane");
expect(resolved.fullName).toBe('"Apocalypse Bane" Dragon');
expect(resolved.tier).toBe(1);
expect(resolved.type).toBe("Hunter");
expect(resolved.power).toBe(20);
expect(resolved.shiny).toBe(true);
expect(resolved.animated).toBe(false);
expect(resolved.rank).toBe(2);
});

it("builds SVG input from render data", () => {
const beast: BeastOnchain = {
tokenId: 1,
beastId: 1,
prefix: 0,
suffix: 0,
level: 1,
health: 5,
shiny: 0,
animated: 0,
rank: 0,
};
const resolved = resolveBeastRenderData(beast);
const svgInput = toBeastSvgInput(resolved);
expect(svgInput.beastId).toBe(1);
expect(svgInput.rank).toBe(0);
});
});
105 changes: 105 additions & 0 deletions sdk/__tests__/images.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, it, expect } from 'vitest';
import {
getBeastImageUrl,
getBeastImageUrls,
getBeastStaticImageUrl,
isLocalBeastImage,
} from '../images';

describe('Image URL generation', () => {
describe('getBeastImageUrl', () => {
it('returns correct path for static regular beast', () => {
expect(getBeastImageUrl(29, false, false)).toBe(
'/images/beasts/static/regular/dragon.png'
);
});

it('returns correct path for static shiny beast', () => {
expect(getBeastImageUrl(29, true, false)).toBe(
'/images/beasts/static/shiny/dragon.png'
);
});

it('returns correct path for animated regular beast', () => {
expect(getBeastImageUrl(29, false, true)).toBe(
'/images/beasts/animated/regular/dragon.gif'
);
});

it('returns correct path for animated shiny beast', () => {
expect(getBeastImageUrl(29, true, true)).toBe(
'/images/beasts/animated/shiny/dragon.gif'
);
});

it('handles multi-word beast names correctly', () => {
// Beast ID 66 is "Nemean Lion" -> should become "nemeanlion"
expect(getBeastImageUrl(66, false, false)).toBe(
'/images/beasts/static/regular/nemeanlion.png'
);
});

it('handles various beast IDs', () => {
// Warlock (ID 1)
expect(getBeastImageUrl(1, false, false)).toBe(
'/images/beasts/static/regular/warlock.png'
);

// Kraken (ID 51)
expect(getBeastImageUrl(51, true, true)).toBe(
'/images/beasts/animated/shiny/kraken.gif'
);

// Fairy (ID 21)
expect(getBeastImageUrl(21, false, false)).toBe(
'/images/beasts/static/regular/fairy.png'
);
});

it('handles unknown beast ID gracefully', () => {
expect(getBeastImageUrl(0, false, false)).toBe(
'/images/beasts/static/regular/unknown.png'
);
expect(getBeastImageUrl(100, false, false)).toBe(
'/images/beasts/static/regular/unknown.png'
);
});
});

describe('getBeastImageUrls', () => {
it('returns both imageUrl and imageSmallUrl', () => {
const result = getBeastImageUrls(29, false, false);
expect(result.imageUrl).toBe('/images/beasts/static/regular/dragon.png');
expect(result.imageSmallUrl).toBe('/images/beasts/static/regular/dragon.png');
});

it('handles shiny and animated flags', () => {
const result = getBeastImageUrls(51, true, true);
expect(result.imageUrl).toBe('/images/beasts/animated/shiny/kraken.gif');
expect(result.imageSmallUrl).toBe('/images/beasts/animated/shiny/kraken.gif');
});
});

describe('getBeastStaticImageUrl', () => {
it('always returns PNG static image', () => {
expect(getBeastStaticImageUrl(29, false)).toBe(
'/images/beasts/static/regular/dragon.png'
);
expect(getBeastStaticImageUrl(29, true)).toBe(
'/images/beasts/static/shiny/dragon.png'
);
});
});

describe('isLocalBeastImage', () => {
it('returns true for local beast image URLs', () => {
expect(isLocalBeastImage('/images/beasts/static/regular/dragon.png')).toBe(true);
expect(isLocalBeastImage('/images/beasts/animated/shiny/kraken.gif')).toBe(true);
});

it('returns false for external URLs', () => {
expect(isLocalBeastImage('https://example.com/image.png')).toBe(false);
expect(isLocalBeastImage('/images/other/image.png')).toBe(false);
});
});
});
140 changes: 140 additions & 0 deletions sdk/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, it, expect } from 'vitest';
import {
getFullBeastName,
isGenesisBeast,
getTierColor,
calculatePower,
getBeastInfo,
} from '../index';

describe('Beast SDK utility functions', () => {
describe('getFullBeastName', () => {
it('returns beast name with prefix and suffix', () => {
expect(getFullBeastName(29, 'Apocalypse', 'Bane')).toBe(
'"Apocalypse Bane" Dragon'
);
});

it('returns beast name with only prefix', () => {
expect(getFullBeastName(29, 'Apocalypse', null)).toBe(
'"Apocalypse" Dragon'
);
});

it('returns beast name with only suffix', () => {
expect(getFullBeastName(29, null, 'Bane')).toBe('"Bane" Dragon');
});

it('returns just beast name when no prefix or suffix', () => {
expect(getFullBeastName(29, null, null)).toBe('Dragon');
});

it('handles unknown beast ID', () => {
expect(getFullBeastName(0, null, null)).toBe('Unknown');
});
});

describe('isGenesisBeast', () => {
it('returns true for token IDs 1-75', () => {
expect(isGenesisBeast(1)).toBe(true);
expect(isGenesisBeast(75)).toBe(true);
expect(isGenesisBeast(38)).toBe(true);
});

it('returns false for token IDs above 75', () => {
expect(isGenesisBeast(76)).toBe(false);
expect(isGenesisBeast(1000)).toBe(false);
expect(isGenesisBeast(99999)).toBe(false);
});

it('returns false for token ID 0', () => {
expect(isGenesisBeast(0)).toBe(false);
});

it('handles string token IDs', () => {
expect(isGenesisBeast('1')).toBe(true);
expect(isGenesisBeast('75')).toBe(true);
expect(isGenesisBeast('76')).toBe(false);
expect(isGenesisBeast('1001')).toBe(false);
});
});

describe('getTierColor', () => {
it('returns orange for tier 1', () => {
expect(getTierColor(1)).toBe('#ff8800');
});

it('returns purple for tier 2', () => {
expect(getTierColor(2)).toBe('#8c00bf');
});

it('returns blue for tier 3', () => {
expect(getTierColor(3)).toBe('#0066ff');
});

it('returns green for tier 4', () => {
expect(getTierColor(4)).toBe('#00cc00');
});

it('returns gray for tier 5', () => {
expect(getTierColor(5)).toBe('#cccccc');
});

it('returns gray for invalid tier', () => {
expect(getTierColor(0)).toBe('#cccccc');
expect(getTierColor(6)).toBe('#cccccc');
});
});

describe('calculatePower', () => {
it('calculates power correctly for tier 1', () => {
// Power = (6 - tier) * level = 5 * level
expect(calculatePower(1, 10)).toBe(50);
expect(calculatePower(1, 20)).toBe(100);
});

it('calculates power correctly for tier 5', () => {
// Power = (6 - 5) * level = 1 * level
expect(calculatePower(5, 10)).toBe(10);
expect(calculatePower(5, 20)).toBe(20);
});

it('calculates power for various tiers', () => {
expect(calculatePower(2, 10)).toBe(40); // (6-2) * 10 = 40
expect(calculatePower(3, 10)).toBe(30); // (6-3) * 10 = 30
expect(calculatePower(4, 10)).toBe(20); // (6-4) * 10 = 20
});
});

describe('getBeastInfo', () => {
it('returns complete info for valid beast ID', () => {
const info = getBeastInfo(29);
expect(info.name).toBe('Dragon');
expect(info.tier).toBe(1);
expect(info.type).toBe('Hunter');
expect(info.tierColor).toBe('#ff8800');
});

it('returns info for Magic beast', () => {
const info = getBeastInfo(1);
expect(info.name).toBe('Warlock');
expect(info.tier).toBe(1);
expect(info.type).toBe('Magic');
});

it('returns info for Brute beast', () => {
const info = getBeastInfo(51);
expect(info.name).toBe('Kraken');
expect(info.tier).toBe(1);
expect(info.type).toBe('Brute');
});

it('returns info for T5 beast', () => {
const info = getBeastInfo(21);
expect(info.name).toBe('Fairy');
expect(info.tier).toBe(5);
expect(info.type).toBe('Magic');
expect(info.tierColor).toBe('#cccccc');
});
});
});
Loading