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
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"typecheck": "tsc --noEmit --incremental false"
},
"dependencies": {
"@filoz/synapse-core": "0.3.3",
"@filoz/synapse-sdk": "0.40.2",
"@ipld/car": "^5.4.2",
"@ipld/dag-pb": "^4.1.4",
Expand Down
125 changes: 125 additions & 0 deletions apps/backend/scripts/create-session-key-safe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Generate a session key and Safe multisig calldata for registering it
* on the SessionKeyRegistry contract.
*
* Usage:
* node scripts/create-session-key-safe.mjs [--network mainnet|calibration] [--expiry-days 90] [--session-key 0x...]
*
* If --session-key is omitted, a random key is generated.
*
* Outputs:
* 1. Session key address and private key
* 2. Permission hashes being registered
* 3. Safe transaction details (target, calldata, value)
* 4. Verification: decoded calldata for review
* 5. Env vars for DealBot deployment
*
* The calldata should be submitted as a custom transaction in the Safe UI
* (app.safe.global) from the DealBot multisig wallet.
*/

import { calibration, mainnet } from "@filoz/synapse-core/chains";
import {
AddPiecesPermission,
CreateDataSetPermission,
DefaultFwssPermissions,
DeleteDataSetPermission,
loginCall,
SchedulePieceRemovalsPermission,
} from "@filoz/synapse-core/session-key";
import { decodeFunctionData, encodeFunctionData } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

// Parse CLI args
const args = process.argv.slice(2);
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
}

const networkName = getArg("--network") || "calibration";
const expiryDays = Number(getArg("--expiry-days") || "90");
const sessionPrivateKey = getArg("--session-key") || generatePrivateKey();

const chain = networkName === "mainnet" ? mainnet : calibration;
const sessionAccount = privateKeyToAccount(sessionPrivateKey);
const expiresAt = BigInt(Math.floor(Date.now() / 1000) + expiryDays * 24 * 60 * 60);
const expiryDate = new Date(Number(expiresAt) * 1000);

// Use the SDK's loginCall to get the exact ABI and args
const call = loginCall({
chain,
address: sessionAccount.address,
permissions: DefaultFwssPermissions,
expiresAt,
origin: "dealbot",
});

// Encode the calldata
const calldata = encodeFunctionData({
abi: call.abi,
functionName: call.functionName,
args: call.args,
});

// Verify by decoding it back
const decoded = decodeFunctionData({
abi: call.abi,
data: calldata,
});

// Permission labels for display
const permissionLabels = {
[CreateDataSetPermission]: "CreateDataSet",
[AddPiecesPermission]: "AddPieces",
[SchedulePieceRemovalsPermission]: "SchedulePieceRemovals",
[DeleteDataSetPermission]: "DeleteDataSet",
};

// Output
console.log("=== Session Key Registration for Safe Multisig ===");
console.log();
console.log(`Network: ${networkName} (chain ${chain.id})`);
console.log(`Session key addr: ${sessionAccount.address}`);
console.log(`Expiry: ${expiryDate.toISOString()} (${expiryDays} days)`);
console.log(`Origin: dealbot`);
console.log();
console.log("--- Permissions ---");
for (const hash of DefaultFwssPermissions) {
console.log(` ${permissionLabels[hash] || "Unknown"}: ${hash}`);
}
console.log();
console.log("--- Safe Transaction ---");
console.log(`Target (SessionKeyRegistry): ${call.address}`);
console.log(`Value: 0`);
console.log(`Calldata:`);
console.log(calldata);
console.log();
console.log("--- Verification (decoded calldata) ---");
console.log(`Function: ${decoded.functionName}`);
console.log(`Args:`);
console.log(` signer: ${decoded.args[0]}`);
console.log(` expiry: ${decoded.args[1]} (${new Date(Number(decoded.args[1]) * 1000).toISOString()})`);
console.log(` permissions: [`);
for (const p of decoded.args[2]) {
console.log(` ${p} (${permissionLabels[p] || "Unknown"})`);
}
console.log(` ]`);
console.log(` origin: "${decoded.args[3]}"`);
console.log();
console.log("--- Safe UI Steps ---");
console.log("1. Go to safe.filecoin.io and open the DealBot multisig");
console.log("2. New Transaction > Transaction Builder");
console.log(`3. Enter contract address: ${call.address}`);
console.log('4. Select "Custom data (hex encoded)"');
console.log("5. Paste the calldata above");
console.log("6. Value: 0");
console.log("7. Review, sign, and collect required signatures");
console.log();
console.log("--- DealBot Env Vars (for SOPS secrets) ---");
console.log(`SESSION_KEY_PRIVATE_KEY=${sessionPrivateKey}`);
console.log();
console.log("--- Renewal ---");
console.log("To renew, run this script again with the same --session-key");
console.log("and submit the new calldata via Safe. The contract overwrites");
console.log("the previous registration for the same signer address.");
110 changes: 110 additions & 0 deletions apps/backend/scripts/fund-safe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Generate Safe multisig calldata for depositing USDFC into Filecoin Pay
* and approving FWSS as an operator.
*
* Usage:
* node scripts/fund-safe.mjs --network mainnet|calibration --amount 50 --wallet-address 0x...
*
* Outputs a 3-transaction batch for the Safe Transaction Builder:
* 1. USDFC.approve(FilecoinPay, amount)
* 2. FilecoinPay.deposit(USDFC, walletAddress, amount)
* 3. FilecoinPay.setOperatorApproval(USDFC, FWSS, true, maxUint256, maxUint256, maxUint256)
*
* Prerequisites: the multisig must hold USDFC tokens (ERC20 balance, not
* Filecoin Pay balance). Transfer USDFC to the multisig first if needed.
*/

import { calibration, mainnet } from "@filoz/synapse-core/chains";
import { encodeFunctionData, parseUnits } from "viem";

const args = process.argv.slice(2);
function getArg(name) {
const idx = args.indexOf(name);
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
}

const networkName = getArg("--network") || "calibration";
const amountStr = getArg("--amount");
const walletAddress = getArg("--wallet-address");

if (!amountStr) {
console.error("--amount is required (e.g. --amount 50 for 50 USDFC)");
process.exit(1);
}
if (!walletAddress) {
console.error("--wallet-address is required (the multisig address to credit)");
process.exit(1);
}

const chain = networkName === "mainnet" ? mainnet : calibration;
const amount = parseUnits(amountStr, 18);
const maxUint256 = 2n ** 256n - 1n;
Comment thread
rvagg marked this conversation as resolved.

const usdfcAddress = chain.contracts.usdfc.address;
const filecoinPayAddress = chain.contracts.filecoinPay.address;
const fwssAddress = chain.contracts.fwss.address;

// Transaction 1: ERC20 approve
const approveCalldata = encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "approve",
args: [filecoinPayAddress, amount],
});

// Transaction 2: deposit
const depositCalldata = encodeFunctionData({
abi: chain.contracts.filecoinPay.abi,
functionName: "deposit",
args: [usdfcAddress, walletAddress, amount],
});

// Transaction 3: setOperatorApproval
const approveOperatorCalldata = encodeFunctionData({
abi: chain.contracts.filecoinPay.abi,
functionName: "setOperatorApproval",
args: [usdfcAddress, fwssAddress, true, maxUint256, maxUint256, maxUint256],
});

console.log("=== Payment Setup for Safe Multisig ===");
console.log();
console.log(`Network: ${networkName} (chain ${chain.id})`);
console.log(`Wallet: ${walletAddress}`);
console.log(`Deposit: ${amountStr} USDFC`);
console.log(`USDFC: ${usdfcAddress}`);
console.log(`FilecoinPay: ${filecoinPayAddress}`);
console.log(`FWSS: ${fwssAddress}`);
console.log();
console.log("--- Transaction 1: Approve USDFC spend ---");
console.log(`Target: ${usdfcAddress}`);
console.log(`Value: 0`);
console.log(`Data: ${approveCalldata}`);
console.log();
console.log("--- Transaction 2: Deposit into Filecoin Pay ---");
console.log(`Target: ${filecoinPayAddress}`);
console.log(`Value: 0`);
console.log(`Data: ${depositCalldata}`);
console.log();
console.log("--- Transaction 3: Approve FWSS operator ---");
console.log(`Target: ${filecoinPayAddress}`);
console.log(`Value: 0`);
console.log(`Data: ${approveOperatorCalldata}`);
console.log();
console.log("--- Safe UI Steps ---");
console.log("1. Go to safe.filecoin.io and open the multisig");
console.log("2. New Transaction > Transaction Builder");
console.log("3. Add all 3 transactions above (target + calldata for each)");
console.log("4. Send Batch, review, sign, and collect required signatures");
console.log();
console.log("Note: The multisig must hold USDFC tokens before executing.");
console.log("Transfer USDFC to the multisig address first if needed.");
9 changes: 6 additions & 3 deletions apps/backend/src/config/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export const configValidationSchema = Joi.object({
// Blockchain
NETWORK: Joi.string().valid("mainnet", "calibration").default("calibration"),
WALLET_ADDRESS: Joi.string().required(),
WALLET_PRIVATE_KEY: Joi.string().required(),
WALLET_PRIVATE_KEY: Joi.string().optional().empty(""),
RPC_URL: Joi.string()
.uri({ scheme: ["http", "https"] })
.optional()
.allow(""),
SESSION_KEY_PRIVATE_KEY: Joi.string().optional().empty(""),
CHECK_DATASET_CREATION_FEES: Joi.boolean().default(true),
USE_ONLY_APPROVED_PROVIDERS: Joi.boolean().default(true),
DEALBOT_DATASET_VERSION: Joi.string().optional(),
Expand Down Expand Up @@ -82,7 +83,7 @@ export const configValidationSchema = Joi.object({
HTTP2_REQUEST_TIMEOUT_MS: Joi.number().min(1000).default(240000), // 4 minutes total for HTTP/2 requests (10MiB @ 170KB/s + overhead)
IPNI_VERIFICATION_TIMEOUT_MS: Joi.number().min(1000).default(60000), // 60 seconds max time to wait for IPNI verification
IPNI_VERIFICATION_POLLING_MS: Joi.number().min(250).default(2000), // 2 seconds between IPNI verification polls
});
}).or("WALLET_PRIVATE_KEY", "SESSION_KEY_PRIVATE_KEY");
Comment thread
rvagg marked this conversation as resolved.

export interface IAppConfig {
env: string;
Expand All @@ -106,6 +107,7 @@ export interface IDatabaseConfig {
export interface IBlockchainConfig {
network: Network;
rpcUrl?: string;
sessionKeyPrivateKey?: `0x${string}`;
walletAddress: string;
walletPrivateKey: `0x${string}`;
checkDatasetCreationFees: boolean;
Expand Down Expand Up @@ -266,8 +268,9 @@ export function loadConfig(): IConfig {
blockchain: {
network: (process.env.NETWORK || "calibration") as Network,
rpcUrl: process.env.RPC_URL || undefined,
sessionKeyPrivateKey: (process.env.SESSION_KEY_PRIVATE_KEY || undefined) as `0x${string}` | undefined,
walletAddress: process.env.WALLET_ADDRESS || "0x0000000000000000000000000000000000000000",
walletPrivateKey: process.env.WALLET_PRIVATE_KEY as "0x${string}",
walletPrivateKey: (process.env.WALLET_PRIVATE_KEY || undefined) as `0x${string}`,
checkDatasetCreationFees: process.env.CHECK_DATASET_CREATION_FEES !== "false",
useOnlyApprovedProviders: process.env.USE_ONLY_APPROVED_PROVIDERS !== "false",
dealbotDataSetVersion: process.env.DEALBOT_DATASET_VERSION,
Expand Down
55 changes: 46 additions & 9 deletions apps/backend/src/deal/deal.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { randomUUID } from "node:crypto";
import * as SessionKey from "@filoz/synapse-core/session-key";
import { calibration, METADATA_KEYS, mainnet, SIZE_CONSTANTS, Synapse } from "@filoz/synapse-sdk";
import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { InjectRepository } from "@nestjs/typeorm";
import { executeUpload } from "filecoin-pin";
import { CID } from "multiformats/cid";
import type { Repository } from "typeorm";
import { http } from "viem";
import { custom, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { awaitWithAbort } from "../common/abort-utils.js";
import { buildUnixfsCar } from "../common/car-utils.js";
Expand Down Expand Up @@ -65,12 +66,12 @@ export class DealService implements OnModuleInit, OnModuleDestroy {
this.blockchainConfig = this.configService.get("blockchain");
}

onModuleInit() {
async onModuleInit() {
this.logger.log({
event: "synapse_initialization",
message: "Creating shared Synapse instance",
});
this.sharedSynapse = this.createSynapseInstance();
this.sharedSynapse = await this.createSynapseInstance();
}

async onModuleDestroy(): Promise<void> {
Expand All @@ -92,7 +93,7 @@ export class DealService implements OnModuleInit, OnModuleDestroy {
const { preprocessed, cleanup } = await this.prepareDealInput(options.signal, options.logContext);

try {
const synapse = this.sharedSynapse ?? this.createSynapseInstance();
const synapse = this.sharedSynapse ?? (await this.createSynapseInstance());
const uploadPayload = await this.prepareUploadPayload(preprocessed, options.signal);
return await this.createDeal(
synapse,
Expand Down Expand Up @@ -478,7 +479,7 @@ export class DealService implements OnModuleInit, OnModuleDestroy {
signal?: AbortSignal,
): Promise<boolean> {
signal?.throwIfAborted();
const synapse = this.sharedSynapse ?? this.createSynapseInstance();
const synapse = this.sharedSynapse ?? (await this.createSynapseInstance());
const providerInfo = this.walletSdkService.getProviderInfo(providerAddress);
if (!providerInfo) {
throw new Error(`Provider ${providerAddress} not found in registry`);
Expand Down Expand Up @@ -537,7 +538,7 @@ export class DealService implements OnModuleInit, OnModuleDestroy {
let transactionHash: string | undefined;

try {
const synapse = this.sharedSynapse ?? this.createSynapseInstance();
const synapse = this.sharedSynapse ?? (await this.createSynapseInstance());
signal?.throwIfAborted();

const DATA_SET_CREATION_PIECE_SIZE = 200 * 1024; // 200 KiB
Expand Down Expand Up @@ -676,13 +677,49 @@ export class DealService implements OnModuleInit, OnModuleDestroy {
// Deal Creation Helpers
// ============================================================================

private createSynapseInstance(): Synapse {
private async createSynapseInstance(): Promise<Synapse> {
try {
const chain = this.blockchainConfig.network === "mainnet" ? mainnet : calibration;
const rpcUrl = this.blockchainConfig.rpcUrl;
const transport = rpcUrl ? http(rpcUrl) : http();
const sessionKeyPK = this.blockchainConfig.sessionKeyPrivateKey;

if (sessionKeyPK) {
// Session key mode: walletAddress is the multisig (payer),
// sessionKeyPrivateKey provides the delegated signing key
const walletAddress = this.blockchainConfig.walletAddress as `0x${string}`;
const sessionKey = SessionKey.fromSecp256k1({
privateKey: sessionKeyPK,
root: walletAddress,
chain,
transport,
});
await sessionKey.syncExpirations();

// Synapse requires a custom transport for address-only (json-rpc) accounts
const resolved = transport({ chain, retryCount: 0 });

this.logger.log({
event: "synapse_session_key_init",
message: "Initializing Synapse with session key",
walletAddress,
sessionKeyAddress: sessionKey.address,
});

return Synapse.create({
account: walletAddress,
chain,
source: "dealbot",
transport: custom({ request: resolved.request }),
sessionKey,
});
}

return Synapse.create({
account: privateKeyToAccount(this.blockchainConfig.walletPrivateKey),
chain: this.blockchainConfig.network === "mainnet" ? mainnet : calibration,
chain,
source: "dealbot",
...(this.blockchainConfig.rpcUrl ? { transport: http(this.blockchainConfig.rpcUrl) } : {}),
...(rpcUrl ? { transport } : {}),
});
} catch (error) {
this.logger.error({
Expand Down
Loading
Loading