Skip to content
Merged
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
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ flowchart LR
subgraph "Linera network"
chain1["user chain"]
chain2["user chain"]
chain3["GoL scoring chain"]
chain3["GoL scoring chain x 4"]
style chain1 fill:#bbf,stroke:#333,stroke-width:1px,color:#000
style chain2 fill:#bbf,stroke:#333,stroke-width:1px,color:#000
end
Expand Down Expand Up @@ -54,13 +54,18 @@ Run the commands below using `bash -e -x <(linera extract-script-from-markdown R

```bash
# Production app
APP_ID="27145fa604adf9996647a9a2add1dafe8f80f1a547835edf62ee408cd8903dd3"
APP_ID=750eb4b947761eeece6c52fd488ec23442dce240fab150b93ea2212b014aaace

# Test user
MATHIEU_CLI="0x359C1a2203aE35adBFA85bC9C1EAB540bF8797a7"
# Scoring chains
CHAIN_0=74b5850ecf6a7389523f7a9748dc6f81fc71533757f617b65e5c9f01fa1430b8
CHAIN_1=3e6bdd095d2e4e30f12e8da38ea1409f2442696b01badbda4226577df09479ff
CHAIN_2=78bfe088e0e6ab2acbb894c7bac4b537650a98ca7337667cef38359b6c590508
CHAIN_3=d6e2e25987b75a51f9ac1df8851bd0e0d16d858e7e2896b1e9d511bac8e13f92

# Scoring chain
CHAIN="e71636fde3a70cdbfdb7fd9bef6cb1ba632af8b0567b8f76df47b35489972dd3"
# Test user and its pinned scoring chain
MATHIEU_CLI="0x359C1a2203aE35adBFA85bC9C1EAB540bF8797a7"
# 2 == 0x359C1a220 % 4
CHAIN=$CHAIN_2

# Getting a chain and tracking the scores
FAUCET_URL=https://faucet.testnet-conway.linera.net
Expand Down
1 change: 0 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ linera_spawn linera net up --policy-config testnet --with-faucet --faucet-port $
```

Create the user wallet and add a chain to it:

```bash
export LINERA_WALLET="$LINERA_TMP_DIR/wallet.json"
export LINERA_KEYSTORE="$LINERA_TMP_DIR/keystore.json"
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/game-of-life/hooks/useCompletedPuzzles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export function useCompletedPuzzles() {
const queryClient = useQueryClient();

const {
data: completedPuzzleIds = [],
data: completedPuzzleIds = new Set<string>(),
refetch,
isLoading: isLoadingFromBlockchain,
} = useQuery({
queryKey: ["completedPuzzles"],
queryFn: async () => {
const lineraService = LineraService.getInstance();
const response = lineraService.getCompletedPuzzleIds();
const response = await lineraService.getCompletedPuzzleIds();
console.log("[GOL] Got completed puzzle IDs", response);
return response;
},
Expand All @@ -25,13 +25,13 @@ export function useCompletedPuzzles() {

const isPuzzleCompleted = useCallback(
(puzzleId: string) => {
return completedPuzzleIds.includes(puzzleId);
return completedPuzzleIds.has(puzzleId);
},
[completedPuzzleIds]
);

const getCompletionCount = useCallback(() => {
return completedPuzzleIds.length;
return completedPuzzleIds.size;
}, [completedPuzzleIds]);

// Refresh the completed puzzles from blockchain
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/lib/linera/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
// Linera network configuration
export const LINERA_RPC_URL = "https://faucet.testnet-conway.linera.net/";

// Game of Life scoring chain IDs
export const GOL_SCORING_CHAIN_IDS = [
"74b5850ecf6a7389523f7a9748dc6f81fc71533757f617b65e5c9f01fa1430b8",
"3e6bdd095d2e4e30f12e8da38ea1409f2442696b01badbda4226577df09479ff",
"78bfe088e0e6ab2acbb894c7bac4b537650a98ca7337667cef38359b6c590508",
"d6e2e25987b75a51f9ac1df8851bd0e0d16d858e7e2896b1e9d511bac8e13f92"
]

// Game of Life application ID
export const GOL_APP_ID = "27145fa604adf9996647a9a2add1dafe8f80f1a547835edf62ee408cd8903dd3";
export const GOL_APP_ID = "750eb4b947761eeece6c52fd488ec23442dce240fab150b93ea2212b014aaace";

// Previous application IDs. (This is used to mark puzzles as solved in the UI.)
export const PREVIOUS_GOL_APP_IDS = ["27145fa604adf9996647a9a2add1dafe8f80f1a547835edf62ee408cd8903dd3"];

// Dynamic wallet configuration (sandbox)
export const DYNAMIC_SANDBOX_ENVIRONMENT_ID = "cf12f3ef-d589-499c-8acb-be7cc211c6e0";
Expand Down
46 changes: 37 additions & 9 deletions frontend/src/lib/linera/lib/linera-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import initLinera, { Faucet, Client, Wallet, Application } from "@linera/client";
import type { Wallet as DynamicWallet } from "@dynamic-labs/sdk-react-core";
import { DynamicSigner } from "./dynamic-signer";
import { LINERA_RPC_URL, GOL_APP_ID } from "../constants";

export interface LineraProvider {
client: Client;
Expand All @@ -15,6 +14,7 @@ export class LineraAdapter {
private static instance: LineraAdapter | null = null;
private provider: LineraProvider | null = null;
private application: Application | null = null;
private previousApplications: Application[] = [];
private wasmInitPromise: Promise<unknown> | null = null;
private connectPromise: Promise<LineraProvider> | null = null;
private onConnectionChange?: () => void;
Expand All @@ -27,7 +27,7 @@ export class LineraAdapter {
return LineraAdapter.instance;
}

async connect(dynamicWallet: DynamicWallet, rpcUrl?: string): Promise<LineraProvider> {
async connect(dynamicWallet: DynamicWallet, rpcUrl: string): Promise<LineraProvider> {
if (this.provider) return this.provider;
if (this.connectPromise) return this.connectPromise;

Expand All @@ -54,13 +54,13 @@ export class LineraAdapter {
}
}

const faucet = await new Faucet(rpcUrl || LINERA_RPC_URL);
const faucet = await new Faucet(rpcUrl);
const wallet = await faucet.createWallet();
const chainId = await faucet.claimChain(wallet, address);

const signer = await new DynamicSigner(dynamicWallet);
const client = await new Client(wallet, signer);
console.log("✅ Linera wallet created successfully!");
console.log("✅ Using Linera chain: ", chainId);

client.onNotification((notification : any) => {
let newBlock = notification.reason.NewBlock;
Expand Down Expand Up @@ -102,14 +102,24 @@ export class LineraAdapter {
}
}

async setApplication(appId?: string) {
async setApplications(appId: string, previousAppIds: string[]) {
if (!this.provider) throw new Error("Not connected to Linera");

const application = await this.provider.client.frontend().application(appId || GOL_APP_ID);
const application = await this.provider.client.frontend().application(appId);

if (!application) throw new Error("Failed to get application");
console.log("✅ Linera application set successfully!");
this.application = application;
console.log("✅ Linera application set successfully: ", appId);

const applications = []
for (const appId of previousAppIds) {
const application = await this.provider.client.frontend().application(appId);
if (!application) throw new Error("Failed to get previous application");
applications.push(application);
}
this.previousApplications = applications;
console.log("✅ Previous Linera application set successfully: ", previousAppIds);

console.log("🔄 Notifying connection state change (app set)");
this.onConnectionChange?.();
}
Expand All @@ -120,8 +130,26 @@ export class LineraAdapter {
const result = await this.application.query(JSON.stringify(query));
const response = JSON.parse(result);

console.log("✅ Linera application queried successfully!");
return response as T;
console.log("✅ Linera application queried successfully: ", response);
return response;
}

async queryCurrentAndPreviousApplications<T>(query: object): Promise<T[]> {
if (!this.application) throw new Error("Application not set");

const responses = [];
const result = await this.application.query(JSON.stringify(query));
const response = JSON.parse(result);
responses.push(response);

for (const application of this.previousApplications) {
const result = await application.query(JSON.stringify(query));
const response = JSON.parse(result);
responses.push(response);
}

console.log("✅ Current and previous Linera applications queried successfully: ", responses);
return responses;
}

getProvider(): LineraProvider {
Expand Down
72 changes: 47 additions & 25 deletions frontend/src/lib/linera/services/LineraService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Wallet as DynamicWallet } from "@dynamic-labs/sdk-react-core";
import { lineraAdapter } from "../lib/linera-adapter";
import { GOL_APP_ID } from "../constants";
import { LINERA_RPC_URL, GOL_APP_ID, PREVIOUS_GOL_APP_IDS, GOL_SCORING_CHAIN_IDS } from "../constants";
import { Puzzle, LineraBoard, ValidationResult, DifficultyLevel } from "@/lib/types/puzzle.types";

export interface WalletInfo {
chainId: string;
createdAt: string;
scoringChainId: string;
}

export class LineraService {
Expand Down Expand Up @@ -33,17 +34,23 @@ export class LineraService {
try {
console.log("Initializing Linera service with Dynamic wallet...");

const provider = await lineraAdapter.connect(dynamicWallet);
const provider = await lineraAdapter.connect(dynamicWallet, LINERA_RPC_URL);

await lineraAdapter.setApplication(GOL_APP_ID);
await lineraAdapter.setApplications(GOL_APP_ID, PREVIOUS_GOL_APP_IDS);

const address = lineraAdapter.getAddress();
const value = Number(address.substring(0, 10)); // top 8 hex digits including 0x
const index = value % GOL_SCORING_CHAIN_IDS.length;
const scoringChainId = GOL_SCORING_CHAIN_IDS[index];

this.walletInfo = {
chainId: provider.chainId,
createdAt: new Date().toISOString(),
scoringChainId: scoringChainId,
};

this.initialized = true;
console.log("Linera service initialized successfully");
console.log("Linera service initialized successfully: ", this.walletInfo);
} catch (error) {
console.error("Failed to initialize Linera service:", error);
throw error;
Expand All @@ -57,7 +64,7 @@ export class LineraService {
}

private async ensureInitialized(): Promise<void> {
if (!this.initialized || !lineraAdapter.isApplicationSet()) {
if (!this.initialized || !lineraAdapter.isApplicationSet() || !this.walletInfo) {
throw new Error("Linera service not initialized");
}
}
Expand Down Expand Up @@ -141,15 +148,15 @@ export class LineraService {

async submitSolution(puzzleId: string, board: LineraBoard): Promise<boolean> {
await this.ensureInitialized();

const scoringChainId = this.getWalletInfo().scoringChainId;
try {
const mutation = {
query: `
mutation SubmitSolution($puzzleId: String!, $board: BoardInput!) {
submitSolution(puzzleId: $puzzleId, board: $board)
mutation SubmitSolution($puzzleId: String!, $board: BoardInput!, $scoringChainId: String!) {
submitSolution(puzzleId: $puzzleId, board: $board, scoringChainId: $scoringChainId)
}
`,
variables: { puzzleId, board },
variables: { puzzleId, board, scoringChainId },
};

const result = await lineraAdapter.queryApplication<any>(mutation);
Expand Down Expand Up @@ -250,23 +257,29 @@ export class LineraService {
variables: { puzzleId },
};

const result = await lineraAdapter.queryApplication<any>(query);
console.log("[GOL] Check puzzle completion response", result);
const results = await lineraAdapter.queryCurrentAndPreviousApplications<any>(query);
console.log("[GOL] Check puzzle completion response", results);

if (result.errors) {
// If there's an error, the solution doesn't exist
return false;
for (const result of results) {
if (result.errors) {
// If there's an error, skip the reponse.
continue
}

// If we have a solution entry, the puzzle is completed
if (result.data?.solutions?.entry !== null) {
return true;
}
}

// If we have a solution entry, the puzzle is completed
return result.data?.solutions?.entry !== null;
return false;
} catch (error) {
console.error("Failed to check puzzle completion:", error);
return false;
}
}

async getCompletedPuzzleIds(): Promise<string[]> {
async getCompletedPuzzleIds(): Promise<Set<string>> {
await this.ensureInitialized();

try {
Expand All @@ -281,20 +294,26 @@ export class LineraService {
variables: {},
};

const result = await lineraAdapter.queryApplication<any>(query);
// console.log("[GOL] Got completed puzzle IDs", result);
const results = await lineraAdapter.queryCurrentAndPreviousApplications<any>(query);
console.log("[GOL] Query sent using address: ", lineraAdapter.getAddress());

if (result.errors) {
console.error("GraphQL errors:", result.errors);
return [];
const keys = new Set<string>();
for (const result of results) {
if (result.errors) {
continue
}

// If we have a solution entry, the puzzle is completed
if (result.data?.solutions?.keys) {
result.data?.solutions?.keys.forEach((item) => keys.add(item));
}
}

// The keys field returns an array of DataBlobHash (puzzle IDs)
return result.data?.solutions?.keys || [];
return keys;
} catch (error) {
console.error("Failed to get completed puzzle IDs:", error);
return [];
return new Set<string>();
}
}

Expand Down Expand Up @@ -339,7 +358,10 @@ export class LineraService {
return cells;
}

getWalletInfo(): WalletInfo | null {
getWalletInfo(): WalletInfo {
if (!this.walletInfo) {
throw new Error("Linera service not initialized");
}
return this.walletInfo;
}
}