Skip to content
Draft
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,460 changes: 5,106 additions & 2,354 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"typescript": "^5.6.3"
},
"dependencies": {
"@safe-global/protocol-kit": "^5.1.0",
"@safe-global/safe-core-sdk": "^3.3.5",
"dotenv": "^16.4.7",
"dotenv-expand": "^12.0.1",
"ethers": "^6.13.4",
Expand Down
21 changes: 11 additions & 10 deletions src/appSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import {SUPPORTED_CHAINS, SupportedChain} from './types';
import {ChainId} from './types';
import getNetwork from './getNetwork';

dotenvExpand.expand(dotenv.config());

Expand All @@ -11,12 +12,20 @@ const appSettings = {
walletPrivateKey: validatePrivateKey(
process.env.WALLET_PRIVATE_KEY ?? missingEnvVar('WALLET_PRIVATE_KEY'),
),
chain: validateChain(process.env.CHAIN ?? missingEnvVar('CHAIN')),

network: getNetwork(
Number(process.env.CHAIN_ID ?? missingEnvVar('CHAIN_ID')) as ChainId,
),

rpcUrl: process.env.RPC_URL ?? missingEnvVar('RPC_URL'),
rpcUrlAccessToken: process.env.RPC_URL_ACCESS_TOKEN,

graphQlUrl: process.env.GRAPHQL_URL ?? missingEnvVar('GRAPHQL_URL'),
graphQlAccessToken:
process.env.GRAPHQL_ACCESS_TOKEN ?? missingEnvVar('GRAPHQL_ACCESS_TOKEN'),

shouldTryAutoTopUp: process.env.SHOULD_TRY_AUTO_TOP_UP === 'true',
safes: process.env.SAFES ? JSON.parse(process.env.SAFES) : {},
} as const;

export default appSettings;
Expand All @@ -25,14 +34,6 @@ function missingEnvVar(name: string): never {
throw new Error(`Missing ${name} in .env file.`);
}

function validateChain(chain: string): SupportedChain {
if (!SUPPORTED_CHAINS.includes(chain as SupportedChain)) {
throw new Error(`Unsupported chain: ${chain}`);
}

return chain as SupportedChain;
}

function validatePrivateKey(key: string): string {
if (!key.match(/^[0-9a-fA-F]{64}$/)) {
throw new Error('Invalid wallet private key format');
Expand Down
16 changes: 7 additions & 9 deletions src/drips-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {Contract, TransactionResponse} from 'ethers';
import appSettings from './appSettings';
import getWalletInstance from './getWalletInstance';

const {chain} = appSettings;

const contractAddresses = {
mainnet: '0xd0Dd053392db676D57317CD4fe96Fc2cCf42D0b4',
filecoin: '0xd320F59F109c618b19707ea5C5F068020eA333B3',
sepolia: '0x74A32a38D945b9527524900429b083547DeB9bF4',
} as const;
const {
network: {
contracts: {drips: contractAddress},
name: networkName,
},
} = appSettings;

let contractInstance: Contract | null = null;

Expand All @@ -25,10 +24,9 @@ async function getDripsContract(): Promise<Contract> {
}

const wallet = await getWalletInstance();
const contractAddress = contractAddresses[chain];

if (!contractAddress) {
throw new Error(`No contract address configured for chain: ${chain}`);
throw new Error(`No contract address configured for chain: ${networkName}`);
}

try {
Expand Down
81 changes: 81 additions & 0 deletions src/ensureWalletHasSufficientBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {formatEther, parseEther, Wallet} from 'ethers';
import Safe from '@safe-global/protocol-kit';
import appSettings from './appSettings';

const {
safes,
rpcUrl,
network: {minBalance, targetBalance, name: networkName},
} = appSettings;

async function requestWithdrawalFromSafe(
wallet: Wallet,
amount: bigint,
): Promise<void> {
const safeAddress = safes[networkName];
if (!safeAddress) {
throw new Error('Safe address not configured');
}

try {
const safeSdk = await Safe.init({
provider: rpcUrl,
safeAddress,
});

const transaction = {
to: wallet.address,
value: amount.toString(),
data: '0x',
};

console.log('Requesting transaction...');
const safeTransaction = await safeSdk.createTransaction({
transactions: [transaction],
});

// console.log('Signing transaction...');
// const signedTransaction = await safeSdk.signTransaction(safeTransaction);
// console.log('Transaction prepared:', signedTransaction);

const executeTxResponse = await safeSdk.executeTransaction(safeTransaction);
console.log('Transaction confirmed. Hash:', executeTxResponse.hash);
} catch (error) {
console.error(
'Failed to withdraw from Safe:',
error instanceof Error ? error.message : error,
);
throw error;
}
}

export async function ensureWalletHasSufficientBalance(
wallet: Wallet,
): Promise<void> {
// Only try to top up the wallet on mainnet.
// if (networkName !== 'mainnet') {
// console.log('Skipping auto top up on non-mainnet network.');
// return;
// }

if (!wallet.provider) {
throw new Error('Wallet provider not configured');
}

console.log('Trying to auto top up wallet balance...');
const balance = await wallet.provider.getBalance(wallet.address);
console.log(`Current wallet balance: ${formatEther(balance)} ETH`);

if (balance < parseEther(minBalance.toString())) {
const neededAmount = parseEther(targetBalance.toString()) - balance;
console.log(
`Low balance (target is ${targetBalance} ETH), withdrawing ${formatEther(neededAmount)} ETH from Safe...`,
);

await requestWithdrawalFromSafe(wallet, neededAmount);
const newBalance = await wallet.provider.getBalance(wallet.address);
console.log(`New wallet balance: ${formatEther(newBalance)} ETH`);
} else {
console.log('Wallet balance is sufficient.');
}
}
47 changes: 47 additions & 0 deletions src/getNetwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ChainId, Network, SUPPORTED_CHAIN_IDS} from './types';

export default function getNetwork(chain: ChainId): Network {
validateChain(chain);

switch (chain) {
case 1:
return {
chainId: 1,
name: 'mainnet',
symbol: 'ETH',
minBalance: 0,
targetBalance: 0,
contracts: {
drips: '0xd0Dd053392db676D57317CD4fe96Fc2cCf42D0b4',
},
};
case 11155111:
return {
chainId: 11155111,
name: 'sepolia',
symbol: 'SepoliaETH',
minBalance: 10,
targetBalance: 10,
contracts: {
drips: '0x74A32a38D945b9527524900429b083547DeB9bF4',
},
};
case 314:
return {
chainId: 314,
name: 'filecoin',
symbol: 'FIL',
minBalance: 0,
targetBalance: 0,
contracts: {
drips: '0xd320F59F109c618b19707ea5C5F068020eA333B3',
},
};
}
}

function validateChain(chainId: number) {
if (!SUPPORTED_CHAIN_IDS.includes(chainId as ChainId)) {
throw new Error(`Unsupported chain ID: ${chainId}`);
}
}
12 changes: 8 additions & 4 deletions src/getWalletInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ export default async function getWalletInstance(): Promise<Wallet> {
}

try {
const {rpcUrl, rpcUrlAccessToken} = appSettings;
const {
rpcUrl,
rpcUrlAccessToken,
network: {name: currentNetwork},
} = appSettings;
const urlOrFetchRequest = rpcUrlAccessToken
? createAuthFetchRequest(rpcUrl, rpcUrlAccessToken)
: rpcUrl;

const provider = new JsonRpcProvider(urlOrFetchRequest);

const network = await provider.getNetwork();
if (network.name !== appSettings.chain) {
const providerNetwork = await provider.getNetwork();
if (providerNetwork.name !== currentNetwork) {
throw new Error(
`Provider connected to ${network.name} but expected ${appSettings.chain}`,
`Provider connected to ${providerNetwork.name} but expected ${currentNetwork}`,
);
}

Expand Down
39 changes: 26 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,32 @@ import {
SplitsReceiver,
WriteOperation,
} from './types';
import {ensureWalletHasSufficientBalance} from './ensureWalletHasSufficientBalance';

const MAX_CYCLES = 1000;
const SCRIPT_ITERATIONS = 3;

async function main(): Promise<void> {
const startTime = Date.now();
const wallet = await getWalletInstance();
const db = new Client({connectionString: appSettings.connectionString});
const allWriteOperations: WriteOperation[] = [];

async function main(db: Client): Promise<void> {
try {
console.log('Starting script...');
await db.connect();
console.log('Connected to database.');
const startTime = Date.now();

const {
network: {symbol},
shouldTryAutoTopUp,
} = appSettings;

const wallet = await getWalletInstance();
const startBalance = await wallet.provider!.getBalance(wallet.address);
console.log(`Initial wallet balance: ${formatEther(startBalance)} ETH`);

if (shouldTryAutoTopUp) {
await ensureWalletHasSufficientBalance(wallet);
}

const allWriteOperations: WriteOperation[] = [];

await db.connect();
console.log('Connected to database.');

const tokens = await getTokens(db);
console.log(`Found ${tokens.length} tokens to process`);
Expand Down Expand Up @@ -58,6 +67,8 @@ async function main(): Promise<void> {

console.log('\n=== Final Results ===');
console.log(`Total cost: ${formatEther(costWei)} ETH`);
console.log(`Starting balance: ${formatEther(startBalance)} ${symbol}`);
console.log(`Current balance: ${formatEther(endBalance)} ${symbol}`);
console.log(
`Total execution time: ${executionTimeMinutes.toFixed(2)} minutes`,
);
Expand Down Expand Up @@ -234,7 +245,9 @@ function logWriteOperations(operations: WriteOperation[]): void {
}
}

void main().catch(error => {
console.error('Unhandled error in main:', error);
process.exit(1); // eslint-disable-line n/no-process-exit
});
void main(new Client({connectionString: appSettings.connectionString})).catch(
error => {
console.error('Unhandled error in main:', error);
process.exit(1); // eslint-disable-line n/no-process-exit
},
);
6 changes: 4 additions & 2 deletions src/queries/getAllDripListsSortedByCreationDate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {Client} from 'pg';
import appSettings from '../appSettings';

const {chain} = appSettings;
const {
network: {name: dbSchema},
} = appSettings;

export async function getAllDripListsSortedByCreationDate(db: Client) {
return db.query<{
id: bigint;
createdAt: Date;
}>({
text: `SELECT * FROM "${chain}"."DripLists" ORDER BY "createdAt" DESC`,
text: `SELECT * FROM "${dbSchema}"."DripLists" ORDER BY "createdAt" DESC`,
});
}
6 changes: 4 additions & 2 deletions src/queries/getAllProjectsSortedByCreationDate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {Client} from 'pg';
import appSettings from '../appSettings';

const {chain} = appSettings;
const {
network: {name: dbSchema},
} = appSettings;

export async function getAllProjectsSortedByCreationDate(db: Client) {
return db.query<{
id: bigint;
createdAt: Date;
}>({
text: `SELECT * FROM "${chain}"."GitProjects" ORDER BY "createdAt" DESC`,
text: `SELECT * FROM "${dbSchema}"."GitProjects" ORDER BY "createdAt" DESC`,
});
}
10 changes: 6 additions & 4 deletions src/queries/getCurrentSplitsReceivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {Client} from 'pg';
import appSettings from '../appSettings';
import {SplitsReceiver} from '../types';

const {chain} = appSettings;
const {
network: {name: dbSchema},
} = appSettings;

type SplitRow = {
fundeeAccountId?: string;
Expand All @@ -20,17 +22,17 @@ export default async function getCurrentSplitsReceivers(
const idColumn = type === 'dripList' ? 'funderDripListId' : 'funderProjectId';

const {rows: addressSplits} = await db.query<SplitRow>({
text: `SELECT * FROM "${chain}"."AddressDriverSplitReceivers" WHERE "${idColumn}" = $1`,
text: `SELECT * FROM "${dbSchema}"."AddressDriverSplitReceivers" WHERE "${idColumn}" = $1`,
values: [accountId],
});

const {rows: dripListSplits} = await db.query<SplitRow>({
text: `SELECT * FROM "${chain}"."DripListSplitReceivers" WHERE "${idColumn}" = $1`,
text: `SELECT * FROM "${dbSchema}"."DripListSplitReceivers" WHERE "${idColumn}" = $1`,
values: [accountId],
});

const {rows: projectSplits} = await db.query<SplitRow>({
text: `SELECT * FROM "${chain}"."RepoDriverSplitReceivers" WHERE "${idColumn}" = $1`,
text: `SELECT * FROM "${dbSchema}"."RepoDriverSplitReceivers" WHERE "${idColumn}" = $1`,
values: [accountId],
});

Expand Down
Loading