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
7 changes: 7 additions & 0 deletions typescript/packages/mechanisms/concordium/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
docs/
dist/
node_modules/
coverage/
.github/
**/**/*.json
*.md
11 changes: 11 additions & 0 deletions typescript/packages/mechanisms/concordium/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"proseWrap": "never"
}
5 changes: 5 additions & 0 deletions typescript/packages/mechanisms/concordium/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @x402/concordium Changelog

## 1.0.0

- Implement x402 v2 protocol support for the Concordium mechanism (exact scheme).
445 changes: 445 additions & 0 deletions typescript/packages/mechanisms/concordium/README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions typescript/packages/mechanisms/concordium/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @x402/concordium
91 changes: 91 additions & 0 deletions typescript/packages/mechanisms/concordium/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import js from "@eslint/js";
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import prettier from "eslint-plugin-prettier";
import jsdoc from "eslint-plugin-jsdoc";
import importPlugin from "eslint-plugin-import";

export default [
{
ignores: ["dist/**", "node_modules/**"],
},
{
files: ["**/*.ts", "**/*.tsx"],
ignores: ["**/*.test.ts", "test/**/*"],
languageOptions: {
parser: tsParser,
sourceType: "module",
ecmaVersion: 2020,
globals: {
process: "readonly",
__dirname: "readonly",
module: "readonly",
require: "readonly",
Buffer: "readonly",
exports: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
},
},
plugins: {
"@typescript-eslint": ts,
prettier: prettier,
jsdoc: jsdoc,
import: importPlugin,
},
rules: {
...ts.configs.recommended.rules,
"import/first": "error",
"prettier/prettier": "error",
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }],
"jsdoc/tag-lines": ["error", "any", { startLines: 1 }],
"jsdoc/check-alignment": "error",
"jsdoc/no-undefined-types": "off",
"jsdoc/check-param-names": "error",
"jsdoc/check-tag-names": "error",
"jsdoc/check-types": "error",
"jsdoc/implements-on-classes": "error",
"jsdoc/require-description": "error",
"jsdoc/require-jsdoc": [
"error",
{
require: {
FunctionDeclaration: true,
MethodDefinition: true,
ClassDeclaration: true,
ArrowFunctionExpression: false,
FunctionExpression: false,
},
},
],
"jsdoc/require-param": "error",
"jsdoc/require-param-description": "error",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns": "error",
"jsdoc/require-returns-description": "error",
"jsdoc/require-returns-type": "off",
"jsdoc/require-hyphen-before-param-description": ["error", "always"],
},
},
{
files: ["**/*.test.ts", "test/**/*"],
languageOptions: {
parser: tsParser,
sourceType: "module",
ecmaVersion: 2020,
},
plugins: {
"@typescript-eslint": ts,
prettier: prettier,
},
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/member-ordering": "off",
},
},
];
96 changes: 96 additions & 0 deletions typescript/packages/mechanisms/concordium/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"name": "@x402/concordium",
"version": "1.0.0",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"scripts": {
"start": "tsx --env-file=.env index.ts",
"build": "tsup",
"test": "vitest run",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:watch": "vitest",
"watch": "tsc --watch",
"format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"",
"format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"",
"lint": "eslint . --ext .ts --fix",
"lint:check": "eslint . --ext .ts"
},
"keywords": [
"x402",
"payment",
"protocol",
"concordium"
],
"license": "Apache-2.0",
"author": "Coinbase Inc.",
"repository": "https://github.com/coinbase/x402",
"description": "x402 Payment Protocol Concordium Implementation",
"devDependencies": {
"@eslint/js": "^9.24.0",
"@types/node": "^22.13.4",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-prettier": "^5.2.6",
"prettier": "3.5.2",
"tsup": "^8.4.0",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vite": "^6.2.6",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.5"
},
"dependencies": {
"@x402/core": "workspace:*",
"@concordium/web-sdk": "^12.0.0",
"zod": "^3.24.2"
},
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.mts",
"default": "./dist/esm/index.mjs"
},
"require": {
"types": "./dist/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./exact/client": {
"import": {
"types": "./dist/esm/exact/client/index.d.mts",
"default": "./dist/esm/exact/client/index.mjs"
},
"require": {
"types": "./dist/cjs/exact/client/index.d.ts",
"default": "./dist/cjs/exact/client/index.js"
}
},
"./exact/server": {
"import": {
"types": "./dist/esm/exact/server/index.d.mts",
"default": "./dist/esm/exact/server/index.mjs"
},
"require": {
"types": "./dist/cjs/exact/server/index.d.ts",
"default": "./dist/cjs/exact/server/index.js"
}
},
"./exact/facilitator": {
"import": {
"types": "./dist/esm/exact/facilitator/index.d.mts",
"default": "./dist/esm/exact/facilitator/index.mjs"
},
"require": {
"types": "./dist/cjs/exact/facilitator/index.d.ts",
"default": "./dist/cjs/exact/facilitator/index.js"
}
}
},
"files": [
"dist"
]
}
112 changes: 112 additions & 0 deletions typescript/packages/mechanisms/concordium/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Network } from "@x402/core/types";

/** CAIP-2 network identifier for Concordium Mainnet */
export const CONCORDIUM_MAINNET_CAIP2: Network = "ccd:9dd9ca4d19e9393877d2c44b70f89acb";

/** CAIP-2 network identifier for Concordium Testnet */
export const CONCORDIUM_TESTNET_CAIP2: Network = "ccd:4221332d34e1694168c2a0c0b3fd0f27";

/** Wildcard matching all Concordium networks */
export const CONCORDIUM_WILDCARD_CAIP2: Network = "ccd:*";

/** Default mainnet gRPC endpoint (host:port) */
export const CONCORDIUM_MAINNET_GRPC = "grpc.mainnet.concordium.software:20000";

/** Default testnet gRPC endpoint (host:port) */
export const CONCORDIUM_TESTNET_GRPC = "grpc.testnet.concordium.com:20000";

/** Default gRPC port */
export const DEFAULT_GRPC_PORT = 20000;

/** Mainnet block explorer base URL */
export const CONCORDIUM_MAINNET_EXPLORER = "https://ccdexplorer.io/mainnet";

/** Testnet block explorer base URL */
export const CONCORDIUM_TESTNET_EXPLORER = "https://ccdexplorer.io/testnet";

/**
* Regex pattern for validating Concordium base58check account addresses.
* Matches 45–55 alphanumeric characters (base58 alphabet, no 0/O/I/l).
*/
export const CONCORDIUM_ADDRESS_REGEX =
/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{45,55}$/;

/** Default decimals for native CCD */
export const CCD_DECIMALS = 6;

/** Native CCD uses empty string as asset identifier */
export const CCD_ASSET_IDENTIFIER = "";

/**
* Maximum allowed transaction expiry offset in seconds (spec Rule 7).
* Transactions with expiry > now + this value are rejected.
*/
export const MAX_EXPIRY_OFFSET_SECONDS = 600;

/**
* Default timeout for waiting for ConcordiumBFT finalization (ms).
* Concordium has ~10s deterministic finality; 60s provides comfortable margin.
*/
export const DEFAULT_FINALIZATION_TIMEOUT_MS = 60_000;

/** Maps CAIP-2 identifiers to gRPC endpoints */
export const CONCORDIUM_NETWORK_TO_GRPC: ReadonlyMap<Network, string> = new Map([
[CONCORDIUM_MAINNET_CAIP2, CONCORDIUM_MAINNET_GRPC],
[CONCORDIUM_TESTNET_CAIP2, CONCORDIUM_TESTNET_GRPC],
]);

/** Maps CAIP-2 identifiers to explorer base URLs */
export const CONCORDIUM_NETWORK_TO_EXPLORER: ReadonlyMap<Network, string> = new Map([
[CONCORDIUM_MAINNET_CAIP2, CONCORDIUM_MAINNET_EXPLORER],
[CONCORDIUM_TESTNET_CAIP2, CONCORDIUM_TESTNET_EXPLORER],
]);

/**
* Gets the gRPC endpoint for a Concordium network.
*
* @param network - CAIP-2 network identifier
* @returns gRPC endpoint string (host:port)
* @throws If the network is not recognized
*/
export function getConcordiumGrpcUrl(network: Network): string {
const url = CONCORDIUM_NETWORK_TO_GRPC.get(network);
if (!url) {
throw new Error(`Unsupported Concordium network: "${network}"`);
}
return url;
}

/**
* Gets the block explorer URL for a transaction.
*
* @param network - CAIP-2 network identifier
* @param txHash - Transaction hash (hex)
* @returns Full explorer URL, or undefined if network not recognized
*/
export function getExplorerTxUrl(network: Network, txHash: string): string | undefined {
const base = CONCORDIUM_NETWORK_TO_EXPLORER.get(network);
return base ? `${base}/transaction/${txHash}` : undefined;
}

/**
* Gets the block explorer URL for an account.
*
* @param network - CAIP-2 network identifier
* @param address - Account address (base58check)
* @returns Full explorer URL, or undefined if network not recognized
*/
export function getExplorerAccountUrl(network: Network, address: string): string | undefined {
const base = CONCORDIUM_NETWORK_TO_EXPLORER.get(network);
return base ? `${base}/account/${address}` : undefined;
}

/**
* Parses a gRPC endpoint string into host and port.
*
* @param grpcUrl - Endpoint in "host:port" format
* @returns Tuple of [host, port]
*/
export function parseGrpcUrl(grpcUrl: string): [host: string, port: number] {
const [host, portStr] = grpcUrl.split(":");
return [host, parseInt(portStr, 10) || DEFAULT_GRPC_PORT];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ExactConcordiumScheme } from "./scheme";
export type { ClientConcordiumConfig } from "./scheme";
Loading
Loading