Skip to content
Closed
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
2 changes: 1 addition & 1 deletion frontend/.astro/data-store.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Users/antoineestienne/GithubRepositories/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"]
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"C:\\\\Users\\\\hp\\\\Desktop\\\\guild\\\\TheGuildGenesis\\\\frontend\\\\node_modules\\\\.astro\\\\sessions\"}}}"]
5 changes: 4 additions & 1 deletion frontend/src/components/AppWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "../lib/wagmi";
import { getWagmiConfig } from "../lib/wagmi";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import {
SidebarProvider,
Expand All @@ -20,6 +20,9 @@
}

export function AppWrapper({ children }: AppWrapperProps) {
// call the config getter inside the client component so it runs in the browser
const config = getWagmiConfig();

Check failure on line 24 in frontend/src/components/AppWrapper.tsx

View workflow job for this annotation

GitHub Actions / Frontend Tests

src/test/AppWrapper.test.tsx > AppWrapper > renders title and connect button

Error: [vitest] No "getWagmiConfig" export is defined on the "@/lib/wagmi" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("@/lib/wagmi"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ AppWrapper src/components/AppWrapper.tsx:24:18 ❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:23863:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5529:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8897:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:10522:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:1522:13 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:15140:22 ❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:14956:41

return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
Expand Down
30 changes: 16 additions & 14 deletions frontend/src/lib/wagmi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { http } from "wagmi";
import { polygonAmoy } from "wagmi/chains";

const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as
| string
| undefined;
console.log(projectId);
export const config = getDefaultConfig({
appName: "The Guild Genesis",
projectId: projectId ?? "",
chains: [polygonAmoy],
ssr: false,
syncConnectedChain: true,
transports: {
[polygonAmoy.id]: http(),
},
});
export function getWagmiConfig() {
const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as
| string
| undefined;

return getDefaultConfig({
appName: "The Guild Genesis",
projectId: projectId ?? "",
chains: [polygonAmoy],
ssr: false,
syncConnectedChain: true,
transports: {
[polygonAmoy.id]: http(),
},
});
}
7 changes: 4 additions & 3 deletions frontend/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import HomePage from '@/components/pages/HomePage';

<Layout title="The Guild Genesis">
<div class="min-h-screen bg-gray-50">
<AppWrapper client:only="react">
<HomePage/>
</AppWrapper>
<!-- Temporary server-rendered fallback: render HomePage directly to test SSR output -->
<div>
<HomePage />
</div>
</div>
</Layout>
47 changes: 47 additions & 0 deletions the-guild-smart-contracts/TGC_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# TheGuild Contribution Token (TGC)

The TheGuild Contribution Token (TGC) is a simple ERC20 token used to reward contributors. It is owner-mintable and supports batch minting with a distribution ID to prevent duplicate distributions.

## Key features

- ERC20 token (symbol: TGC)
- Owner-only minting
- `mintWithReason(to, amount, reason)` — mints and emits `ContributionTokenMinted(to, amount, reason)` where `reason` is a `bytes32` reference (e.g., a ticket id)
- `batchMint(distributionId, recipients[], amounts[], reasons[])` — mints to many recipients in one call and marks `distributionId` as executed so it cannot be re-used. Use this to ensure a CSV or distribution is only applied once.

## `distributionId`

`distributionId` should be a unique identifier for a distribution batch. Recommended approaches:

- `keccak256(bytes(csvContents))` — compute a hash of the CSV file contents and use that as the id.
- `keccak256(abi.encodePacked(fileName, timestamp))` — if you want to include a timestamp.

The contract tracks which `distributionId`s have been executed and will revert if the same id is passed twice.

## CSV format and scripts

Scripts in the `script/` folder support minting via CSV input. CSV format:
```
<address>,<amount>,<hex32_reason>
```
- `address` — recipient address (0x...)
- `amount` — integer amount (the Node helper multiplies by token decimals by default)
- `hex32_reason` — a 0x-prefixed hex string up to 32 bytes. You can also provide a plain string and the Node helper will hex-encode it.

Examples:
- `script/DeployAndMintTGC.s.sol` — deploys a new TGC and batch mints the CSV contents (computes `distributionId = keccak256(csv)`).
- `script/MintTGCExisting.s.sol` — attaches to an existing TGC address and batch mints the CSV contents.
- `script/mint_from_csv.js` — Node helper that performs batched `batchMint` calls. Useful for large lists and retries.

## Tests

There are unit tests under `test/` that verify `mintWithReason`, `batchMint`, and the `distributionId` guard.

## GitHub issue workflow (example)

1. Contributor closes issue #65 and requests reward.
2. Admin prepares a CSV with recipients, amounts, and reasons (include the issue id in reason where useful).
3. Compute `distributionId = keccak256(csvContents)` and run the deploy/mint script.
4. The contract will mark the `distributionId` executed so you cannot accidentally re-run the same distribution.

If you want, I can add an example script which computes `distributionId` and calls the Forge script automatically, or a small GitHub Action that runs the Node helper on a CSV uploaded to the repo.
154 changes: 154 additions & 0 deletions the-guild-smart-contracts/script/DeployAndMintTGC.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {stdJson} from "forge-std/StdJson.sol";
import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol";

contract DeployAndMintTGC is Script {
function run() public {
// CSV file path relative to project root
string memory csvPath = "script/data/initial_tgc_mints.csv";

vm.startBroadcast();

TheGuildContributionToken token = new TheGuildContributionToken();

// read CSV and prepare arrays for batchMint
string memory csv = vm.readFile(csvPath);
string[] memory lines = csvSplit(csv);

// count non-empty lines
uint256 count = 0;
for (uint256 i = 0; i < lines.length; i++) if (bytes(lines[i]).length != 0) count++;

address[] memory recipients = new address[](count);
uint256[] memory amounts = new uint256[](count);
bytes32[] memory reasons = new bytes32[](count);

uint256 idx = 0;
for (uint256 i = 0; i < lines.length; i++) {
string memory line = lines[i];
if (bytes(line).length == 0) continue;
(address recipient, uint256 amount, bytes32 reason) = parseLine(line);
recipients[idx] = recipient;
amounts[idx] = amount;
reasons[idx] = reason;
idx++;
}

// compute distributionId as keccak256(csv)
bytes32 distributionId = keccak256(bytes(csv));

// perform batch mint
token.batchMint(distributionId, recipients, amounts, reasons);

vm.stopBroadcast();
}

// --- small CSV parser helpers ---
function csvSplit(string memory s) internal pure returns (string[] memory) {
// naive split on '\n'
bytes memory b = bytes(s);
uint256 linesCount = 1;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == "\n") linesCount++;
}
string[] memory parts = new string[](linesCount);
uint256 idx = 0;
bytes memory cur;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == "\n") {
parts[idx] = string(cur);
idx++;
cur = "";
} else {
cur = abi.encodePacked(cur, b[i]);
}
}
// last
if (cur.length != 0) {
parts[idx] = string(cur);
}
return parts;
}

function parseLine(string memory line) internal pure returns (address, uint256, bytes32) {
// naive split by comma
bytes memory b = bytes(line);
string[] memory cols = new string[](3);
uint256 col = 0;
bytes memory cur;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == ",") {
cols[col] = string(cur);
col++;
cur = "";
} else {
cur = abi.encodePacked(cur, b[i]);
}
}
if (cur.length != 0) cols[col] = string(cur);

address recipient = parseAddr(cols[0]);
uint256 amount = parseUint(cols[1]);
bytes32 reason = parseBytes32Hex(cols[2]);
return (recipient, amount, reason);
}

function parseAddr(string memory s) internal pure returns (address) {
bytes memory bb = bytes(s);
// fallback to address(0) if empty
if (bb.length == 0) return address(0);
return parseAddrFromHex(s);
}

function parseAddrFromHex(string memory s) internal pure returns (address) {
bytes memory _s = bytes(s);
uint256 start = 0;
if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2;
require(_s.length - start == 40, "INVALID_ADDR_LENGTH");
uint160 addr = 0;
for (uint256 i = start; i < _s.length; i++) {
addr <<= 4;
uint8 c = uint8(_s[i]);
if (c >= 48 && c <= 57) addr |= uint160(c - 48);
else if (c >= 65 && c <= 70) addr |= uint160(c - 55);
else if (c >= 97 && c <= 102) addr |= uint160(c - 87);
else revert("INVALID_HEX_CHAR");
}
return address(addr);
}

function parseUint(string memory s) internal pure returns (uint256) {
bytes memory b = bytes(s);
uint256 n = 0;
for (uint256 i = 0; i < b.length; i++) {
uint8 c = uint8(b[i]);
if (c >= 48 && c <= 57) n = n * 10 + (c - 48);
}
return n;
}

function parseBytes32Hex(string memory s) internal pure returns (bytes32) {
bytes memory _s = bytes(s);
if (_s.length == 0) return bytes32(0);
uint256 start = 0;
if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2;
bytes32 res = bytes32(0);
uint256 chars = _s.length - start;
// read up to 64 hex chars (32 bytes)
uint256 toRead = chars > 64 ? 64 : chars;
for (uint256 i = 0; i < toRead; i++) {
res <<= 4;
uint8 c = uint8(_s[start + i]);
if (c >= 48 && c <= 57) res |= bytes32(uint256(c - 48));
else if (c >= 65 && c <= 70) res |= bytes32(uint256(c - 55));
else if (c >= 97 && c <= 102) res |= bytes32(uint256(c - 87));
else revert("INVALID_HEX_CHAR");
}
// shift left to make it right-aligned? Keep as parsed (big-endian)
return res;
}
}
103 changes: 103 additions & 0 deletions the-guild-smart-contracts/script/MintTGCExisting.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol";

contract MintTGCExisting is Script {
function run(address tokenAddress) public {
string memory csvPath = "script/data/initial_tgc_mints.csv";

TheGuildContributionToken token = TheGuildContributionToken(tokenAddress);

vm.startBroadcast();

string memory csv = vm.readFile(csvPath);
string[] memory lines = csvSplit(csv);

uint256 count = 0;
for (uint256 i = 0; i < lines.length; i++) if (bytes(lines[i]).length != 0) count++;

address[] memory recipients = new address[](count);
uint256[] memory amounts = new uint256[](count);
bytes32[] memory reasons = new bytes32[](count);

uint256 idx = 0;
for (uint256 i = 0; i < lines.length; i++) {
string memory line = lines[i];
if (bytes(line).length == 0) continue;
(address recipient, uint256 amount, bytes32 reason) = parseLine(line);
recipients[idx] = recipient;
amounts[idx] = amount;
reasons[idx] = reason;
idx++;
}

bytes32 distributionId = keccak256(bytes(csv));
token.batchMint(distributionId, recipients, amounts, reasons);

vm.stopBroadcast();
}

// reuse simple parsers from deploy script
function csvSplit(string memory s) internal pure returns (string[] memory) {
bytes memory b = bytes(s);
uint256 linesCount = 1;
for (uint256 i = 0; i < b.length; i++) if (b[i] == "\n") linesCount++;
string[] memory parts = new string[](linesCount);
uint256 idx = 0;
bytes memory cur;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == "\n") { parts[idx] = string(cur); idx++; cur = ""; }
else { cur = abi.encodePacked(cur, b[i]); }
}
if (cur.length != 0) parts[idx] = string(cur);
return parts;
}

function parseLine(string memory line) internal pure returns (address, uint256, bytes32) {
bytes memory b = bytes(line);
string[] memory cols = new string[](3);
uint256 col = 0;
bytes memory cur;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] == ",") { cols[col] = string(cur); col++; cur = ""; }
else { cur = abi.encodePacked(cur, b[i]); }
}
if (cur.length != 0) cols[col] = string(cur);
return (parseAddrFromHex(cols[0]), parseUint(cols[1]), parseBytes32Hex(cols[2]));
}

function parseAddrFromHex(string memory s) internal pure returns (address) {
bytes memory _s = bytes(s);
uint256 start = 0;
if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2;
require(_s.length - start == 40, "INVALID_ADDR_LENGTH");
uint160 addr = 0;
for (uint256 i = start; i < _s.length; i++) {
addr <<= 4;
uint8 c = uint8(_s[i]);
if (c >= 48 && c <= 57) addr |= uint160(c - 48);
else if (c >= 65 && c <= 70) addr |= uint160(c - 55);
else if (c >= 97 && c <= 102) addr |= uint160(c - 87);
else revert("INVALID_HEX_CHAR");
}
return address(addr);
}

function parseUint(string memory s) internal pure returns (uint256) {
bytes memory b = bytes(s); uint256 n = 0; for (uint256 i = 0; i < b.length; i++) { uint8 c = uint8(b[i]); if (c >= 48 && c <= 57) n = n * 10 + (c - 48); } return n;
}

function parseBytes32Hex(string memory s) internal pure returns (bytes32) {
bytes memory _s = bytes(s);
if (_s.length == 0) return bytes32(0);
uint256 start = 0;
if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2;
bytes32 res = bytes32(0);
uint256 chars = _s.length - start; uint256 toRead = chars > 64 ? 64 : chars;
for (uint256 i = 0; i < toRead; i++) { res <<= 4; uint8 c = uint8(_s[start + i]); if (c >= 48 && c <= 57) res |= bytes32(uint256(c - 48)); else if (c >= 65 && c <= 70) res |= bytes32(uint256(c - 55)); else if (c >= 97 && c <= 102) res |= bytes32(uint256(c - 87)); else revert("INVALID_HEX_CHAR"); }
return res;
}
}
Loading
Loading