diff --git a/packages/minimal-token/daml.yaml b/packages/minimal-token/daml.yaml index 7681bbb..7a8a31f 100644 --- a/packages/minimal-token/daml.yaml +++ b/packages/minimal-token/daml.yaml @@ -1,4 +1,5 @@ -sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 +sdk-version: 3.3.0-snapshot.20250507.0 +#sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: minimal-token version: 0.1.0 source: daml diff --git a/packages/token-sdk/package.json b/packages/token-sdk/package.json index a028766..77a1fa6 100644 --- a/packages/token-sdk/package.json +++ b/packages/token-sdk/package.json @@ -55,6 +55,7 @@ }, "devDependencies": { "@canton-network/core-ledger-client": "^0.17.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.13.10", "@typescript/analyze-trace": "^0.10.1", "@veraswap/esbuild-config": "latest", @@ -75,6 +76,8 @@ }, "dependencies": { "@canton-network/wallet-sdk": "^0.16.0", + "dotenv": "^16.4.7", + "jsonwebtoken": "^9.0.3", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "uuid": "^13.0.0" diff --git a/packages/token-sdk/src/checkDars.ts b/packages/token-sdk/src/checkDars.ts new file mode 100644 index 0000000..e279522 --- /dev/null +++ b/packages/token-sdk/src/checkDars.ts @@ -0,0 +1,27 @@ +import { MINIMAL_TOKEN_PACKAGE_ID } from "./constants/MINIMAL_TOKEN_PACKAGE_ID.js"; +import { getDefaultSdkAndConnect } from "./sdkHelpers.js"; + +export async function checkDars() { + const sdk = await getDefaultSdkAndConnect(); + await sdk.connectAdmin(); + + const isDarUploaded = await sdk.userLedger?.isPackageUploaded( + MINIMAL_TOKEN_PACKAGE_ID + ); + + if (isDarUploaded) { + console.info("minimal-token DAR already uploaded"); + } else { + console.info("minimal-token DAR not uploaded"); + } +} + +checkDars() + .then(() => { + console.info("Done"); + process.exit(0); + }) + .catch((error) => { + console.error("Error in checkDars: ", error); + process.exit(1); + }); diff --git a/packages/token-sdk/src/helpers/get5NAuthController.ts b/packages/token-sdk/src/helpers/get5NAuthController.ts new file mode 100644 index 0000000..f66877f --- /dev/null +++ b/packages/token-sdk/src/helpers/get5NAuthController.ts @@ -0,0 +1,67 @@ +import { AuthController } from "@canton-network/wallet-sdk"; +import jwt from "jsonwebtoken"; +import { get5NToken } from "./get5NToken.js"; + +export const get5NAuthController = ({ + clientId, + clientSecret, + audience, +}: { + clientId: string; + clientSecret: string; + audience?: string; +}): AuthController => { + audience = audience ?? clientId; + + let userAccessToken: string | undefined = undefined; + + const isJwtValid = (token: string): boolean => { + const payload = jwt.decode(token, { json: true }); + if (!payload) return false; + + const now = Math.floor(Date.now() / 1000); + return typeof payload.exp === "number" && payload.exp > now; + }; + + return { + userId: clientId, + getUserToken: async () => { + const cachedAccessToken = userAccessToken; + if (cachedAccessToken && isJwtValid(cachedAccessToken)) { + return { userId: clientId, accessToken: cachedAccessToken }; + } + + const tokenResponse = await get5NToken({ + clientId, + clientSecret, + audience, + }); + + userAccessToken = tokenResponse.access_token; + + return { + userId: clientId, + accessToken: tokenResponse.access_token, + }; + }, + getAdminToken: async () => { + const cachedAccessToken = userAccessToken; + if (cachedAccessToken && isJwtValid(cachedAccessToken)) { + return { userId: clientId, accessToken: cachedAccessToken }; + } + + const tokenResponse = await get5NToken({ + clientId, + clientSecret, + audience, + }); + + userAccessToken = tokenResponse.access_token; + + return { + userId: clientId, + accessToken: tokenResponse.access_token, + }; + }, + }; +}; diff --git a/packages/token-sdk/src/helpers/get5NToken.ts b/packages/token-sdk/src/helpers/get5NToken.ts new file mode 100644 index 0000000..bea65a9 --- /dev/null +++ b/packages/token-sdk/src/helpers/get5NToken.ts @@ -0,0 +1,56 @@ +export interface TokenResponse5N { + access_token: string; + token_type: string; + scope: string; + expires_in: number; + id_token: string; +} + +export async function get5NToken({ + clientId, + clientSecret, + audience, +}: { + clientId: string; + clientSecret: string; + audience?: string; +}) { + if (!clientId || !clientSecret) { + throw new Error( + "CLIENT_ID_5N and CLIENT_SECRET_5N environment variables must be set" + ); + } + + const url = "https://auth.sandbox.fivenorth.io/application/o/token/"; + const headers = { + "Content-Type": "application/x-www-form-urlencoded", + }; + + const body = new URLSearchParams({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + audience: audience ?? clientId, // Often, audience is the client_id itself for client_credentials + scope: "daml_ledger_api", + }).toString(); + + try { + const response = await fetch(url, { + method: "POST", + headers: headers, + body: body, + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error( + `HTTP error! Status: ${response.status}, Details: ${errorData}` + ); + } + + return (await response.json()) as TokenResponse5N; + } catch (error) { + console.error("Failed to obtain 5N token:", error); + throw error; + } +} diff --git a/packages/token-sdk/src/sdkHelpers.ts b/packages/token-sdk/src/sdkHelpers.ts index 7945326..246d5c3 100644 --- a/packages/token-sdk/src/sdkHelpers.ts +++ b/packages/token-sdk/src/sdkHelpers.ts @@ -1,20 +1,86 @@ import { + Config, localNetAuthDefault, localNetLedgerDefault, localNetTokenStandardDefault, WalletSDKImpl, WalletSDK, + AuthTokenProvider, + LedgerController, + TokenStandardController, + ValidatorController, } from "@canton-network/wallet-sdk"; +import { get5NAuthController } from "./helpers/get5NAuthController.js"; + +import dotenv from "dotenv"; + +dotenv.config(); + +export const FIVEN_SCAN_PROXY_API_URL = new URL( + "https://wallet.validator.devnet.sandbox.fivenorth.io/api/validator" +); + +export const FIVEN_LEDGER_API_URL = new URL( + "https://ledger-api.validator.devnet.sandbox.fivenorth.io/" +); + +export const FIVEN_VALIDATOR_API_URL = new URL( + "https://wallet.validator.devnet.sandbox.fivenorth.io/" +); + +const USE_5N = process.env.USE_5N === "true"; +const CLIENT_ID_5N = process.env.CLIENT_ID_5N ?? ""; +const CLIENT_SECRET_5N = process.env.CLIENT_SECRET_5N ?? ""; + +const defaultSdkConfig: Config = { + logger: console, + authFactory: localNetAuthDefault, + ledgerFactory: localNetLedgerDefault, + tokenStandardFactory: localNetTokenStandardDefault, +}; + +const fiveNSdkConfig: Config = { + logger: console, + authFactory: () => + get5NAuthController({ + clientId: CLIENT_ID_5N, + clientSecret: CLIENT_SECRET_5N, + }), + ledgerFactory: ( + userId: string, + authTokenProvider: AuthTokenProvider, + isAdmin = false + ) => + new LedgerController( + userId, + FIVEN_LEDGER_API_URL, + undefined, + isAdmin, + authTokenProvider + ), + tokenStandardFactory: ( + userId: string, + authTokenProvider: AuthTokenProvider + ) => + new TokenStandardController( + userId, + FIVEN_LEDGER_API_URL, + FIVEN_VALIDATOR_API_URL, + undefined, + authTokenProvider + ), + validatorFactory: (userId: string, authTokenProvider: AuthTokenProvider) => + new ValidatorController( + userId, + FIVEN_VALIDATOR_API_URL, + authTokenProvider + ), +}; export const getDefaultSdk = () => - new WalletSDKImpl().configure({ - logger: console, - authFactory: localNetAuthDefault, - ledgerFactory: localNetLedgerDefault, - tokenStandardFactory: localNetTokenStandardDefault, - }); + new WalletSDKImpl().configure(USE_5N ? fiveNSdkConfig : defaultSdkConfig); -const LOCALNET_SCAN_PROXY_API_URL = new URL( +export const LOCALNET_SCAN_PROXY_API_URL = new URL( "http://localhost:2000/api/validator" ); @@ -29,7 +95,9 @@ export const getSdkForParty = async (partyId: string): Promise => { const sdkPromise = (async () => { const sdk = getDefaultSdk(); await sdk.connect(); - await sdk.connectTopology(LOCALNET_SCAN_PROXY_API_URL); + await sdk.connectTopology( + USE_5N ? FIVEN_SCAN_PROXY_API_URL : LOCALNET_SCAN_PROXY_API_URL + ); await sdk.setPartyId(partyId); return sdk; })(); @@ -46,7 +114,8 @@ export const getSdkForParty = async (partyId: string): Promise => { export const getDefaultSdkAndConnect = async () => { const sdk = getDefaultSdk(); await sdk.connect(); - // await sdk.connectTopology(localNetStaticConfig.LOCALNET_SCAN_PROXY_API_URL); - await sdk.connectTopology(LOCALNET_SCAN_PROXY_API_URL); + await sdk.connectTopology( + USE_5N ? FIVEN_SCAN_PROXY_API_URL : LOCALNET_SCAN_PROXY_API_URL + ); return sdk; }; diff --git a/packages/token-sdk/src/testScripts/createUsers.ts b/packages/token-sdk/src/testScripts/createUsers.ts new file mode 100644 index 0000000..cffe8af --- /dev/null +++ b/packages/token-sdk/src/testScripts/createUsers.ts @@ -0,0 +1,40 @@ +import { getDefaultSdkAndConnect } from "../sdkHelpers.js"; + +const PRIMARY_PARTY_ID = process.env.PRIMARY_PARTY_ID; + +async function createUsers() { + if (!PRIMARY_PARTY_ID) { + throw new Error("PRIMARY_PARTY_ID is not set in environment variables"); + } + + const sdk = await getDefaultSdkAndConnect(); + await sdk.connectAdmin(); + const adminLedger = sdk.adminLedger!; + + const userAlice = await adminLedger.createUser( + "minimal-token-alice", + PRIMARY_PARTY_ID + ); + + const userBob = await adminLedger.createUser( + "minimal-token-bob", + PRIMARY_PARTY_ID + ); + + const userCharlie = await adminLedger.createUser( + "minimal-token-charlie", + PRIMARY_PARTY_ID + ); + + console.log({ userAlice, userBob, userCharlie }); +} + +createUsers() + .then(() => { + console.info("Done"); + process.exit(0); + }) + .catch((error) => { + console.error("Error in createUsers: ", error); + process.exit(1); + }); diff --git a/packages/token-sdk/src/testScripts/hello.ts b/packages/token-sdk/src/testScripts/hello.ts index 1ef51af..fde32bf 100644 --- a/packages/token-sdk/src/testScripts/hello.ts +++ b/packages/token-sdk/src/testScripts/hello.ts @@ -5,7 +5,6 @@ import { getWrappedSdkWithKeyPair } from "../wrappedSdk/wrappedSdk.js"; async function hello() { const sdk = await getDefaultSdkAndConnect(); - await sdk.connectAdmin(); // NOTE: this is of course for testing const aliceKeyPair = keyPairFromSeed("alice"); diff --git a/packages/token-sdk/src/testScripts/print5NToken.ts b/packages/token-sdk/src/testScripts/print5NToken.ts new file mode 100644 index 0000000..dd733f9 --- /dev/null +++ b/packages/token-sdk/src/testScripts/print5NToken.ts @@ -0,0 +1,25 @@ +import dotenv from "dotenv"; +import { get5NToken } from "../helpers/get5NToken.js"; + +dotenv.config(); + +async function print5NToken() { + const CLIENT_ID_5N = process.env.CLIENT_ID_5N ?? ""; + const CLIENT_SECRET_5N = process.env.CLIENT_SECRET_5N ?? ""; + + const token = await get5NToken({ + clientId: CLIENT_ID_5N, + clientSecret: CLIENT_SECRET_5N, + }); + + console.log(token.access_token); +} + +print5NToken() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error("Error in get5NToken: ", error); + process.exit(1); + }); diff --git a/packages/token-sdk/src/uploadDars.ts b/packages/token-sdk/src/uploadDars.ts index 6148942..8677528 100644 --- a/packages/token-sdk/src/uploadDars.ts +++ b/packages/token-sdk/src/uploadDars.ts @@ -1,24 +1,11 @@ -import { - localNetAuthDefault, - localNetLedgerDefault, - localNetTokenStandardDefault, - WalletSDKImpl, -} from "@canton-network/wallet-sdk"; import fs from "fs/promises"; import path from "path"; import { MINIMAL_TOKEN_PACKAGE_ID } from "./constants/MINIMAL_TOKEN_PACKAGE_ID.js"; - -const sdk = new WalletSDKImpl().configure({ - logger: console, - authFactory: localNetAuthDefault, - ledgerFactory: localNetLedgerDefault, - tokenStandardFactory: localNetTokenStandardDefault, -}); +import { getDefaultSdkAndConnect } from "./sdkHelpers.js"; export async function uploadDars() { - await sdk.connect(); + const sdk = await getDefaultSdkAndConnect(); await sdk.connectAdmin(); - await sdk.connectTopology(new URL("http://localhost:2000/api/validator")); const isDarUploaded = await sdk.userLedger?.isPackageUploaded( MINIMAL_TOKEN_PACKAGE_ID diff --git a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts index e175c53..b2b1955 100644 --- a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts +++ b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleEffect.ts @@ -2,24 +2,35 @@ import { LedgerController } from "@canton-network/wallet-sdk"; import { ActiveContractResponse } from "../../types/ActiveContractResponse.js"; import { ContractId, Party } from "../../types/daml.js"; import { bondLifecycleEffectTemplateId } from "../../constants/templateIds.js"; +import { InstrumentId } from "../../types/InstrumentId.js"; + +export type LifecycleEventType = "CouponPayment" | "Redemption"; export interface BondLifecycleEffectParams { - producedVersion: string | null; - eventType: "CouponPayment" | "Redemption"; + issuer: Party; + depository: Party; + eventType: LifecycleEventType; targetInstrumentId: string; targetVersion: string; - eventDate: string; + producedVersion?: string; + eventDate: number; + settlementTime?: number; amount: number; + currencyInstrumentId: InstrumentId; } export interface BondLifecycleEffect { contractId: ContractId; - producedVersion: string | null; - eventType: "CouponPayment" | "Redemption"; + issuer: Party; + depository: Party; + eventType: LifecycleEventType; targetInstrumentId: string; targetVersion: string; - eventDate: string; + producedVersion?: string; + eventDate: number; + settlementTime?: number; amount: number; + currencyInstrumentId: InstrumentId; } export async function getLatestBondLifecycleEffect( @@ -77,12 +88,7 @@ export async function getAllBondLifecycleEffects( return { contractId, - producedVersion: createArg.producedVersion, - eventType: createArg.eventType, - targetInstrumentId: createArg.targetInstrumentId, - targetVersion: createArg.targetVersion, - eventDate: createArg.eventDate, - amount: createArg.amount, + ...createArg, }; }) .filter( diff --git a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts index d5865a5..49ab109 100644 --- a/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts +++ b/packages/token-sdk/src/wrappedSdk/bonds/lifecycleInstruction.ts @@ -12,21 +12,7 @@ import { ActiveContractResponse } from "../../types/ActiveContractResponse.js"; import { getContractDisclosure } from "../contractDisclosure.js"; import { InstrumentId } from "../../types/InstrumentId.js"; import { CreatedEvent } from "../../types/CreatedEvent.js"; - -export type LifecycleEventType = "CouponPayment" | "Redemption"; - -export interface BondLifecycleEffect { - issuer: Party; - depository: Party; - eventType: LifecycleEventType; - targetInstrumentId: string; - targetVersion: string; - producedVersion?: string; - eventDate: number; - settlementTime?: number; - amount: number; - currencyInstrumentId: InstrumentId; -} +import { LifecycleEventType } from "./lifecycleEffect.js"; export interface BondLifecycleInstructionParams { eventType: LifecycleEventType; diff --git a/packages/token-sdk/src/wrappedSdk/tokenFactory.ts b/packages/token-sdk/src/wrappedSdk/tokenFactory.ts index a8e8f3d..91b1961 100644 --- a/packages/token-sdk/src/wrappedSdk/tokenFactory.ts +++ b/packages/token-sdk/src/wrappedSdk/tokenFactory.ts @@ -14,7 +14,7 @@ export interface TokenFactoryParams { export const tokenFactoryTemplateId = "#minimal-token:MyTokenFactory:MyTokenFactory"; -const getCreateTokenFactoryCommand = (params: TokenFactoryParams) => +export const getCreateTokenFactoryCommand = (params: TokenFactoryParams) => getCreateCommand({ templateId: tokenFactoryTemplateId, params }); // TODO: do not pass userKeyPair here diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96967e3..a9bbc19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,6 +218,12 @@ importers: '@canton-network/wallet-sdk': specifier: ^0.16.0 version: 0.16.0 + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -231,6 +237,9 @@ importers: '@canton-network/core-ledger-client': specifier: ^0.17.0 version: 0.17.0 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^22.13.10 version: 22.19.1 @@ -2032,6 +2041,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/lodash@4.17.21': resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} @@ -2579,6 +2591,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3007,6 +3022,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + effect@3.19.8: resolution: {integrity: sha512-OmLw8EfH02vdmyU2fO4uY9He/wepwKI5E/JNpE2pseaWWUbaYOK9UlxIiKP20ZEqQr+S/jSqRDGmpiqD/2DeCQ==} @@ -3965,10 +3983,20 @@ packages: engines: {node: '>=10'} hasBin: true + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4078,9 +4106,30 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -7460,6 +7509,11 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.10.1 + '@types/lodash@4.17.21': {} '@types/minimatch@3.0.5': {} @@ -8106,6 +8160,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -8548,6 +8604,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + effect@3.19.8: dependencies: '@standard-schema/spec': 1.0.0 @@ -9708,6 +9768,19 @@ snapshots: jsonparse: 1.3.1 through2: 4.0.2 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -9715,6 +9788,17 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -9802,8 +9886,22 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.startcase@4.4.0: {} lodash.template@4.5.0: diff --git a/scripts/src/lib/utils.ts b/scripts/src/lib/utils.ts index 29364df..e182aee 100644 --- a/scripts/src/lib/utils.ts +++ b/scripts/src/lib/utils.ts @@ -1,137 +1,138 @@ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import zlib from 'zlib' -import { pipeline } from 'stream/promises' -import crypto from 'crypto' -import * as fs from 'fs' -import * as path from 'path' -import * as process from 'process' -import { white, green, italic, red, yellow, bold } from 'yoctocolors' -import * as jsonc from 'jsonc-parser' -import * as tar from 'tar-fs' -import { exec } from 'child_process' -import { promisify } from 'util' - -const ex = promisify(exec) - -export const info = (message: string): string => italic(white(message)) -export const warn = (message: string): string => bold(yellow(message)) -export const error = (message: string): string => bold(red(message)) -export const success = (message: string): string => green(message) +import zlib from "zlib"; +import { pipeline } from "stream/promises"; +import crypto from "crypto"; +import * as fs from "fs"; +import * as path from "path"; +import * as process from "process"; +import { white, green, italic, red, yellow, bold } from "yoctocolors"; +import * as jsonc from "jsonc-parser"; +import * as tar from "tar-fs"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const ex = promisify(exec); + +export const info = (message: string): string => italic(white(message)); +export const warn = (message: string): string => bold(yellow(message)); +export const error = (message: string): string => bold(red(message)); +export const success = (message: string): string => green(message); export const trimNewline = (message: string): string => - message.replace(/\n$/, '') - -const repoRoot = getRepoRoot() -export const CANTON_PATH = path.join(repoRoot, '.canton') -export const SPLICE_PATH = path.join(repoRoot, '.splice') -export const SPLICE_SPEC_PATH = path.join(repoRoot, '.splice-spec') -export const CANTON_BIN = path.join(CANTON_PATH, 'bin/canton') -export const CANTON_CONF = path.join(repoRoot, 'canton/canton.conf') -export const CANTON_BOOTSTRAP = path.join(repoRoot, 'canton/bootstrap.canton') -export const API_SPECS_PATH = path.join(repoRoot, 'api-specs') -export const UTILS_FILE_PATH = path.join(repoRoot, 'scripts/src/lib/utils.ts') + message.replace(/\n$/, ""); + +const repoRoot = getRepoRoot(); +export const CANTON_PATH = path.join(repoRoot, ".canton"); +export const SPLICE_PATH = path.join(repoRoot, ".splice"); +export const SPLICE_SPEC_PATH = path.join(repoRoot, ".splice-spec"); +export const CANTON_BIN = path.join(CANTON_PATH, "bin/canton"); +export const CANTON_CONF = path.join(repoRoot, "canton/canton.conf"); +export const CANTON_BOOTSTRAP = path.join(repoRoot, "canton/bootstrap.canton"); +export const API_SPECS_PATH = path.join(repoRoot, "api-specs"); +export const UTILS_FILE_PATH = path.join(repoRoot, "scripts/src/lib/utils.ts"); export type CantonVersionAndHash = { - version: string - hash: string -} + version: string; + hash: string; +}; // Canton versions -export const DAML_RELEASE_VERSION = '3.3.0-snapshot.20251108.16145.0.v21f4ad7f' +// export const DAML_RELEASE_VERSION = '3.3.0-snapshot.20251108.16145.0.v21f4ad7f' +export const DAML_RELEASE_VERSION = "3.3.0-snapshot.20250507.0"; export const LOCALNET_ARCHIVE_HASH = - '706c4412d1cb29285fe8a591e74f44458d2afcbb04603c32cdb6c6260538145f' + "706c4412d1cb29285fe8a591e74f44458d2afcbb04603c32cdb6c6260538145f"; export const SPLICE_ARCHIVE_HASH = - 'dbe943a466f06624c2f55e2e4ad66180e81804bbcb0288b6a4882df49702a4b1' + "dbe943a466f06624c2f55e2e4ad66180e81804bbcb0288b6a4882df49702a4b1"; export const SPLICE_SPEC_ARCHIVE_HASH = - '102dba4a7224a0acc2544111ecdf2e6538de2b29bcc3bd7348edf4b445e07329' + "102dba4a7224a0acc2544111ecdf2e6538de2b29bcc3bd7348edf4b445e07329"; export const CANTON_ARCHIVE_HASH = - '43c89d9833886fc68cac4951ba1959b7f6cc5269abfff1ba5129859203aa8cd3' -export const SPLICE_VERSION = '0.4.25' + "43c89d9833886fc68cac4951ba1959b7f6cc5269abfff1ba5129859203aa8cd3"; +export const SPLICE_VERSION = "0.4.25"; export const SUPPORTED_VERSIONS = { devnet: { canton: { - version: '3.4.0-snapshot.20250922.16951.0.v1eb3f268', - hash: 'e0f59a7b5015b56479ef4786662c5935a0fee9ac803465bb0f70bdc6c3bf4dff', + version: "3.4.0-snapshot.20250922.16951.0.v1eb3f268", + hash: "e0f59a7b5015b56479ef4786662c5935a0fee9ac803465bb0f70bdc6c3bf4dff", }, }, mainnet: { canton: { - version: '3.3.0-snapshot.20250910.16087.0.v82d35a4d', - hash: '43c89d9833886fc68cac4951ba1959b7f6cc5269abfff1ba5129859203aa8cd3', + version: "3.3.0-snapshot.20250910.16087.0.v82d35a4d", + hash: "43c89d9833886fc68cac4951ba1959b7f6cc5269abfff1ba5129859203aa8cd3", }, }, -} +}; export async function downloadToFile( url: string | URL, directory: string, hash?: string ) { - const filename = path.basename(url.toString()) - const res = await fetch(url) + const filename = path.basename(url.toString()); + const res = await fetch(url); if (!res.ok || !res.body) { - throw new Error(`Failed to download: ${url}`) + throw new Error(`Failed to download: ${url}`); } - await ensureDir(directory) + await ensureDir(directory); await pipeline( res.body, fs.createWriteStream(path.join(directory, filename)) - ) + ); if (hash) { await verifyFileIntegrity( path.join(directory, filename), hash, - 'sha256' - ) + "sha256" + ); } } export async function verifyFileIntegrity( filePath: string, expectedHash: string, - algo = 'sha256' + algo = "sha256" ): Promise { if (!fs.existsSync(filePath)) { - return false + return false; } - const computedHash = await computeFileHash(filePath, algo) + const computedHash = await computeFileHash(filePath, algo); if (computedHash === expectedHash) { console.log( success( `${algo.toUpperCase()} checksum verification of ${filePath} successful.` ) - ) + ); } else { console.log( error( `File hashes did not match.\n\tExpected: ${expectedHash}\n\tReceived: ${computedHash}\nDeleting ${filePath}...` ) - ) + ); //process.exit(1) } - return true + return true; } async function computeFileHash( filePath: string, - algo = 'sha256' + algo = "sha256" ): Promise { return new Promise((resolve, reject) => { - const hash = crypto.createHash(algo) - const stream = fs.createReadStream(filePath) - - stream.on('data', (chunk) => hash.update(chunk)) - stream.on('end', () => { - const computedHash = hash.digest('hex') - resolve(computedHash) - }) - stream.on('error', (err) => reject(err)) - }) + const hash = crypto.createHash(algo); + const stream = fs.createReadStream(filePath); + + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("end", () => { + const computedHash = hash.digest("hex"); + resolve(computedHash); + }); + stream.on("error", (err) => reject(err)); + }); } export async function downloadAndUnpackTarball( @@ -140,157 +141,161 @@ export async function downloadAndUnpackTarball( unpackDir: string, options?: { hash?: string; strip?: number; updateHash?: boolean } ) { - let shouldDownload = true - let currentHash = options?.hash - const algo = 'sha256' + let shouldDownload = true; + let currentHash = options?.hash; + const algo = "sha256"; - ensureDir(path.dirname(tarfile)) - ensureDir(path.dirname(unpackDir)) + ensureDir(path.dirname(tarfile)); + ensureDir(path.dirname(unpackDir)); if (fs.existsSync(tarfile) && options?.hash) { // File exists, check hash - const validFile = await verifyFileIntegrity(tarfile, options.hash, algo) - shouldDownload = !validFile + const validFile = await verifyFileIntegrity( + tarfile, + options.hash, + algo + ); + shouldDownload = !validFile; } if (shouldDownload) { - console.log(info(`Downloading tarball from ${url} to ${tarfile}...`)) - const res = await fetch(url) + console.log(info(`Downloading tarball from ${url} to ${tarfile}...`)); + const res = await fetch(url); if (!res.ok || !res.body) { - throw new Error(`Failed to download: ${url}`) + throw new Error(`Failed to download: ${url}`); } - await pipeline(res.body, fs.createWriteStream(tarfile)) - console.log(success('Download complete.')) + await pipeline(res.body, fs.createWriteStream(tarfile)); + console.log(success("Download complete.")); } if (options?.updateHash) { - const newHash = await computeFileHash(tarfile, algo) + const newHash = await computeFileHash(tarfile, algo); // Update the hash in utils.ts if present - const fileContent = fs.readFileSync(UTILS_FILE_PATH, 'utf8') + const fileContent = fs.readFileSync(UTILS_FILE_PATH, "utf8"); // Find the old hash in the file (matching the old value) if (options?.hash && fileContent.includes(options.hash)) { - const updatedContent = fileContent.replace(options.hash, newHash) + const updatedContent = fileContent.replace(options.hash, newHash); if (updatedContent !== fileContent) { - fs.writeFileSync(UTILS_FILE_PATH, updatedContent, 'utf8') - console.log(success(`Updated hash in utils.ts to ${newHash}`)) + fs.writeFileSync(UTILS_FILE_PATH, updatedContent, "utf8"); + console.log(success(`Updated hash in utils.ts to ${newHash}`)); } } else { console.log( - warn('Old hash not found in utils.ts, no update performed.') - ) + warn("Old hash not found in utils.ts, no update performed.") + ); } - currentHash = newHash + currentHash = newHash; } if (!options?.updateHash && currentHash) { - const validFile = await verifyFileIntegrity(tarfile, currentHash, algo) + const validFile = await verifyFileIntegrity(tarfile, currentHash, algo); const downloadedHash = crypto .createHash(algo) .update(fs.readFileSync(tarfile)) - .digest('hex') + .digest("hex"); if (!validFile) { // Remove the bad file throw new Error( error( `Checksum mismatch for downloaded tarball.\n\tExpected: ${currentHash}\n\tReceived: ${downloadedHash}` ) - ) + ); } } - await ensureDir(unpackDir) + await ensureDir(unpackDir); await pipeline( fs.createReadStream(tarfile), zlib.createGunzip(), tar.extract(unpackDir, { strip: options?.strip ?? 1 }) - ) - console.log(success(`Unpacked tarball into ${unpackDir}`)) + ); + console.log(success(`Unpacked tarball into ${unpackDir}`)); } // Get the root of the current repository // Assumption: the root of the repository is the closest // ancestor directory of the CWD that contains a .git directory export function getRepoRoot(): string { - const cwd = process.cwd() - const segments = cwd.split('/') + const cwd = process.cwd(); + const segments = cwd.split("/"); for (let i = segments.length; i > 0; i--) { - const potentialRoot = segments.slice(0, i).join('/') - if (fs.existsSync(path.join(potentialRoot, '.git'))) { - return potentialRoot + const potentialRoot = segments.slice(0, i).join("/"); + if (fs.existsSync(path.join(potentialRoot, ".git"))) { + return potentialRoot; } } console.error( error(`${cwd} does not seem to be inside a valid git repository.`) - ) - process.exit(1) + ); + process.exit(1); } export function findJsonKeyPosition( jsonContent: string, key: string ): { line: number; column: number } { - const keyPath = key.split('.') - let found: { line: number; column: number } | null = null + const keyPath = key.split("."); + let found: { line: number; column: number } | null = null; function search(node: jsonc.Node, pathIdx: number) { - if (!node || found) return - if (node.type === 'object') { + if (!node || found) return; + if (node.type === "object") { for (const prop of node.children ?? []) { - if (prop.type === 'property' && prop.children?.[0]?.value) { - const propName = prop.children[0].value as string - const isLast = pathIdx === keyPath.length - 1 + if (prop.type === "property" && prop.children?.[0]?.value) { + const propName = prop.children[0].value as string; + const isLast = pathIdx === keyPath.length - 1; const matches = isLast ? propName.startsWith(keyPath[pathIdx]) - : propName === keyPath[pathIdx] + : propName === keyPath[pathIdx]; // If matches, advance pathIdx if (matches) { if (isLast) { - const offset = prop.children[0].offset - const before = jsonContent.slice(0, offset) - const lines = before.split('\n') + const offset = prop.children[0].offset; + const before = jsonContent.slice(0, offset); + const lines = before.split("\n"); found = { line: lines.length, column: lines[lines.length - 1].length + 1, - } - return + }; + return; } else if (prop.children[1]) { - search(prop.children[1], pathIdx + 1) + search(prop.children[1], pathIdx + 1); } } // Always search deeper with the same pathIdx (skip intermediate keys) if (prop.children[1]) { - search(prop.children[1], pathIdx) + search(prop.children[1], pathIdx); } } } - } else if (node.type === 'array') { + } else if (node.type === "array") { for (const child of node.children ?? []) { - search(child, pathIdx) + search(child, pathIdx); } } } - const root = jsonc.parseTree(jsonContent) - if (root) search(root, 0) + const root = jsonc.parseTree(jsonContent); + if (root) search(root, 0); - return found ?? { line: 1, column: 1 } + return found ?? { line: 1, column: 1 }; } export function traverseDirectory( directory: string, callback: (filePath: string) => void ): void { - const entries = fs.readdirSync(directory) + const entries = fs.readdirSync(directory); for (const entry of entries) { - const fullPath = path.join(directory, entry) + const fullPath = path.join(directory, entry); if (fs.statSync(fullPath).isDirectory()) { - traverseDirectory(fullPath, callback) + traverseDirectory(fullPath, callback); } else { - callback(fullPath) + callback(fullPath); } } } @@ -301,37 +306,37 @@ export function getAllFilesWithExtension( ext?: string, recursive = true ): string[] { - let results: string[] = [] - const list = fs.readdirSync(dir) + let results: string[] = []; + const list = fs.readdirSync(dir); for (const file of list) { - const filePath = path.join(dir, file) - const stat = fs.statSync(filePath) + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { if (recursive) { results = results.concat( getAllFilesWithExtension(filePath, ext, true) - ) + ); } } else if (ext === undefined || filePath.endsWith(ext)) { - results.push(filePath) + results.push(filePath); } } - return results + return results; } // Ensure a directory exists export async function ensureDir(dir: string) { if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) + fs.mkdirSync(dir, { recursive: true }); } } // Copy a file export async function copyFileRecursive(src: string, dest: string) { - fs.copyFileSync(src, dest) + fs.copyFileSync(src, dest); } -export type markingLevel = 'info' | 'warn' | 'error' +export type markingLevel = "info" | "warn" | "error"; export function markFile( relativePath: string, fileContent: string, @@ -339,21 +344,21 @@ export function markFile( warning: string, markingLevel: markingLevel ): void { - const typePosition = findJsonKeyPosition(fileContent, key) - const line = typePosition.line || 1 - const column = typePosition.column || 1 - if (markingLevel === 'error') { + const typePosition = findJsonKeyPosition(fileContent, key); + const line = typePosition.line || 1; + const column = typePosition.column || 1; + if (markingLevel === "error") { console.error( `::error file=${relativePath},line=${line},col=${column}::${warning}` - ) - } else if (markingLevel === 'warn') { + ); + } else if (markingLevel === "warn") { console.warn( `::warning file=${relativePath},line=${line},col=${column}::${warning}` - ) - } else if (markingLevel === 'info') { + ); + } else if (markingLevel === "info") { console.info( `::info file=${relativePath},line=${line},col=${column}::${warning}` - ) + ); } } @@ -370,10 +375,10 @@ export function mapObject( ): Record { return Object.fromEntries( Object.entries(obj).map(([k, v]) => { - const mapped = mapFn(k, v) - return Array.isArray(mapped) ? mapped : [k, mapped] + const mapped = mapFn(k, v); + return Array.isArray(mapped) ? mapped : [k, mapped]; }) - ) + ); } /** @@ -384,19 +389,19 @@ export function mapObject( * @returns the elided string */ export function elideMiddle(s: string, len = 8) { - const elider = '...' - const totalLen = s.length + const elider = "..."; + const totalLen = s.length; - if (totalLen <= len) return s - if (len <= elider.length) return s + if (totalLen <= len) return s; + if (len <= elider.length) return s; - const halfway = Math.floor(totalLen / 2) + const halfway = Math.floor(totalLen / 2); return ( s.slice(0, halfway - Math.floor(len / 2)) + elider + s.slice(halfway + Math.floor(len / 2)) - ) + ); } /** @@ -407,39 +412,39 @@ export async function getAllNxDependencies( ): Promise { const projects: string[] = await ex(`yarn nx show projects --json`, { cwd: repoRoot, - }).then(({ stdout }) => JSON.parse(stdout)) + }).then(({ stdout }) => JSON.parse(stdout)); if (!projects.includes(projectName)) { - throw new Error(`Project ${projectName} does not exist.`) + throw new Error(`Project ${projectName} does not exist.`); } interface NxGraph { - nodes: Record - dependencies: Record + nodes: Record; + dependencies: Record; } const { nodes, dependencies }: NxGraph = await ex( `yarn nx graph --print --focus=${projectName}`, { cwd: repoRoot } - ).then(({ stdout }) => JSON.parse(stdout).graph) + ).then(({ stdout }) => JSON.parse(stdout).graph); // Nx shows both child dependencies and parent (reverse) dependencies for the focused package. // Filter out reverse dependencies. const childDependencies: string[] = Object.entries(dependencies).reduce< string[] >((prev, current) => { - const [key, value] = current + const [key, value] = current; if (value.every((dep) => dep.target !== projectName)) { - prev.push(key) + prev.push(key); } - return prev - }, []) + return prev; + }, []); // Nx shows dependencies (example: @daml.js) that are not published to npm // Filter to only public packages (those tagged with "npm:public"). const publicDependencies: string[] = childDependencies.filter((dep) => { - return nodes[dep].data.tags.includes('npm:public') - }) + return nodes[dep].data.tags.includes("npm:public"); + }); - return publicDependencies + return publicDependencies; }