Skip to content

Commit 2460fa7

Browse files
authored
Add batcher and local wallet, do NFT indexing ourselves (#122)
- Remove external indexer dependencies in favor of Paima primitives - Add Hardhat configuration for AnnotatedMintNft contract, like Tarochi for Genesis Trainers - Use `Tarochi Genesis Trainer` entry in `extensions.$ENV.yml` to track NFT ownership - Track per-NFT win/lose stats in a new table, according to what NFT was selected when a lobby was created or joined - Fulfill relevant API routes: - `/account-nfts` - Includes all linked wallets - `/historical-owner` (currently ignores passed-in contract address and block height) - `/title-image` (currently ignores passed-in contract address) - `/nft-score` (currently ignores passed-in contract address) - Add LocalWallet and wallet delegation handling - Add batcher startup configuration - Old middleware method `userWalletLogin` returns as soon as LocalWallet is ready - LocalWallet and batcher are used for most game transactions instead of requiring signing each time - New middleware method `externalWalletConnect` follows full delegation procedure and posts `&wd|` message to batcher - All API routes use "main wallet" when sensible - Update a few more misc NPM packages Future work: - Test NFT-less play in non-debug builds - Test/refine "main wallet" handling in API for corner cases, such as playing some NFT-less games then connecting a wallet Depends on: PaimaStudios/paima-engine#439
1 parent 68a8578 commit 2460fa7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+22087
-15232
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
root = true
2+
3+
[*.{js,ts}]
4+
indent_size = 2
5+
indent_style = space

.env.devnet

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ STOP_BLOCKHEIGHT=
2525
SERVER_ONLY_MODE="false"
2626

2727
# STF checkpoints
28-
LOBBY_AUTOPLAY_BLOCKHEIGHT="14076947"
2928

3029
# DATABASE
3130
DB_NAME="postgres"
@@ -41,4 +40,4 @@ WEBSERVER_PORT="3333"
4140
# CONFIG IDS
4241
SHORT_CONFIG="FWJv579eJfdg0P"
4342
MEDIUM_CONFIG="S6BZo8biAtrDp1"
44-
LONG_CONFIG="sEyGPliPidNUzx"
43+
LONG_CONFIG="sEyGPliPidNUzx"

.env.mainnet

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ STOP_BLOCKHEIGHT=
2525
SERVER_ONLY_MODE="false"
2626

2727
#STF checkpoints
28-
LOBBY_AUTOPLAY_BLOCKHEIGHT="12731234"
2928

3029
# DATABASE
3130
DB_NAME="postgres"

.env.test

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ STOP_BLOCKHEIGHT=
2525
SERVER_ONLY_MODE="false"
2626

2727
# STF checkpoints
28-
LOBBY_AUTOPLAY_BLOCKHEIGHT="14076947"
2928

3029
# DATABASE
3130
DB_NAME="postgres"

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//formatting is done by prettier ran by eslint
33
"editor.formatOnSave": false,
44
"editor.codeActionsOnSave": {
5-
"source.fixAll.eslint": true
5+
"source.fixAll.eslint": "explicit"
66
},
77
"files.associations": {
88
".env.*": "properties"

api/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
"author": "",
1313
"license": "ISC",
1414
"dependencies": {
15+
"@paima/utils-backend": "^5.0.0",
1516
"@tower-defense/db": "../db",
1617
"@tower-defense/game-logic": "../game-logic",
1718
"@tower-defense/utils": "../utils",
1819
"fp-ts": "^2.12.3",
1920
"io-ts": "^2.2.18",
20-
"tsoa": "^6.4.0"
21+
"tsoa": "^6.4.0",
22+
"viem": "^2.21.32"
2123
}
2224
}

api/src/controllers/accountNfts.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { getOwnedNfts } from '@paima/utils-backend';
2+
import { requirePool } from '@tower-defense/db';
3+
import { Controller, Get, Query, Route } from 'tsoa';
4+
import { cdeName, getContractAddress, getNftMetadata } from '@tower-defense/utils';
5+
import { getMainAddress, getRelatedWallets } from '@paima/db';
6+
7+
interface AccountNftsResult {
8+
metadata: {
9+
name: string;
10+
image: string;
11+
};
12+
contract: string;
13+
tokenId: number;
14+
}
15+
16+
interface AccountNftsData {
17+
pages: number;
18+
totalItems: number;
19+
result: AccountNftsResult[];
20+
}
21+
22+
@Route('account-nfts')
23+
export class AccountNftsController extends Controller {
24+
@Get()
25+
public async get(
26+
@Query() account: string,
27+
@Query() page: number,
28+
@Query() size: number
29+
): Promise<{ response: AccountNftsData }> {
30+
console.log('account-nfts', account, page, size);
31+
32+
const pool = requirePool();
33+
account = (await getMainAddress(account, pool)).address;
34+
35+
const related = await getRelatedWallets(account, pool);
36+
const allAddresses = [
37+
...related.from.map(x => x.to_address),
38+
account,
39+
...related.to.map(x => x.from_address),
40+
];
41+
42+
let tokenIds = (await Promise.all(allAddresses.map(x => getOwnedNfts(pool, cdeName, x))))
43+
.flat()
44+
.sort();
45+
46+
const totalItems = tokenIds.length,
47+
pages = Math.ceil(totalItems / size);
48+
tokenIds = tokenIds.slice(page * size, (page + 1) * size);
49+
50+
return {
51+
response: {
52+
pages,
53+
totalItems,
54+
result: tokenIds.map(id => ({
55+
metadata: getNftMetadata(id),
56+
contract: getContractAddress(),
57+
tokenId: Number(id),
58+
})),
59+
},
60+
};
61+
}
62+
}

api/src/controllers/allConfigs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Controller, Get, Query, Route } from 'tsoa';
22
import type { IGetUserConfigsResult } from '@tower-defense/db';
33
import { getUserConfigs } from '@tower-defense/db';
44
import { requirePool } from '@tower-defense/db';
5+
import { getMainAddress } from '@paima/db';
56

67
interface UserConfigsResponse {
78
configs: IGetUserConfigsResult[];
@@ -12,6 +13,7 @@ export class userConfigsController extends Controller {
1213
@Get()
1314
public async get(@Query() creator: string): Promise<UserConfigsResponse> {
1415
const pool = requirePool();
16+
creator = (await getMainAddress(creator, pool)).address;
1517
const configs = await getUserConfigs.run({ creator }, pool);
1618
return { configs };
1719
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Controller, Get, Query, Route } from 'tsoa';
2+
import { getNftOwner } from '@paima/utils-backend';
3+
import { requirePool } from '@tower-defense/db';
4+
import { cdeName } from '@tower-defense/utils';
5+
6+
type HistoricalOwner =
7+
| {
8+
success: true;
9+
result: string;
10+
}
11+
| { success: false };
12+
13+
@Route('historical-owner')
14+
export class HistoricalOwnerController extends Controller {
15+
@Get()
16+
public async get(
17+
@Query() contract: string,
18+
@Query() tokenId: number,
19+
@Query() blockHeight: number
20+
): Promise<HistoricalOwner> {
21+
console.log('historical-owner', contract, tokenId, blockHeight);
22+
23+
// NOTE: This is not a REAL historical owner endpoint! Block height is ignored!
24+
// This is fine for now because the frontend only asks about the current state anyways.
25+
// TODO: It also currently ignores the contract address.
26+
27+
const pool = requirePool();
28+
const value = await getNftOwner(pool, cdeName, BigInt(tokenId));
29+
30+
return value
31+
? {
32+
success: true,
33+
result: value,
34+
}
35+
: { success: false };
36+
}
37+
}

api/src/controllers/nftScore.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { getNftScore, requirePool } from '@tower-defense/db';
2+
import { Controller, Get, Query, Route } from 'tsoa';
3+
import { cdeName } from '@tower-defense/utils';
4+
5+
interface NftScore {
6+
data: {
7+
nft_contract: string;
8+
token_id: number;
9+
total_games: number;
10+
wins: number;
11+
draws: number;
12+
losses: number;
13+
score: number;
14+
};
15+
}
16+
17+
@Route('nft-score')
18+
export class NftScoreController extends Controller {
19+
@Get()
20+
public async get(@Query() nft_contract: string, @Query() token_id: number): Promise<NftScore> {
21+
console.log('nft-score', nft_contract, token_id);
22+
23+
const pool = requirePool();
24+
const rows = await getNftScore.run({ cde_name: cdeName, token_id: String(token_id) }, pool);
25+
const { wins, losses } = rows.length > 0 ? rows[0] : { wins: 0, losses: 0 };
26+
27+
return {
28+
data: {
29+
nft_contract,
30+
token_id,
31+
wins,
32+
losses,
33+
// These are not shown in the frontend, except for a sanity check that
34+
// totalGames must be equal to wins + losses + draws.
35+
total_games: wins + losses,
36+
draws: 0,
37+
score: wins,
38+
},
39+
};
40+
}
41+
}

0 commit comments

Comments
 (0)