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 .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
target
.snfoundry_cache/
snfoundry_trace/
coverage/
profile/
.env
node_modules/
dist/
24 changes: 24 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "erc_contract"
version = "0.1.0"
dependencies = [
"snforge_std",
]

[[package]]
name = "snforge_scarb_plugin"
version = "0.60.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:924358bf316e502923f6733b50e239ea37585a05dc24c5fc8dd9e45f88cf7339"

[[package]]
name = "snforge_std"
version = "0.60.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:32e6baabec4f9af21089bc7ca685ffea5e4164497340ecbdb99314e568029195"
dependencies = [
"snforge_scarb_plugin",
]
52 changes: 52 additions & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
name = "erc_contract"
version = "0.1.0"
edition = "2024_07"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet = "2.18.0"

[dev-dependencies]
snforge_std = "0.60.0"
assert_macros = "2.18.0"

[[target.starknet-contract]]
sierra = true

[scripts]
test = "snforge test"

[tool.scarb]
allow-prebuilt-plugins = ["snforge_std"]

# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information

# [tool.snforge] # Define `snforge` tool section
# exit_first = true # Stop tests execution immediately upon the first failure
# fuzzer_runs = 1234 # Number of runs of the random fuzzer
# fuzzer_seed = 1111 # Seed for the random fuzzer

# [[tool.snforge.fork]] # Used for fork testing
# name = "SOME_NAME" # Fork name
# url = "http://your.rpc.url" # Url of the RPC provider
# block_id.tag = "latest" # Block to fork from (block tag)

# [[tool.snforge.fork]]
# name = "SOME_SECOND_NAME"
# url = "http://your.second.rpc.url"
# block_id.number = "123" # Block to fork from (block number)

# [[tool.snforge.fork]]
# name = "SOME_THIRD_NAME"
# url = "http://your.third.rpc.url"
# block_id.hash = "0x123" # Block to fork from (block hash)

# [profile.dev.cairo] # Configure Cairo compiler
# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage
# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler
# inlining-strategy = "avoid" # Should be used if you want to use coverage

# [features] # Used for conditional compilation
# enable_for_tests = [] # Feature name and list of other features that should be enabled with it
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@starknet-agentic/transfer-agent",
"version": "1.0.0",
"description": "Autonomous transfer agent for Starknet",
"main": "dist/index.js",
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc"
},
"dependencies": {
"dotenv": "^16.4.5",
"starknet": "^9.4.2"
},
"devDependencies": {
"@types/node": "^20.12.7",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}
11 changes: 11 additions & 0 deletions snfoundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html
# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information

# [sncast.default] # Define a profile name
# url = "https://api.zan.top/public/starknet-sepolia/rpc/v0_10" # Url of the RPC provider
# accounts-file = "../account-file" # Path to the file with the account data
# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions
# keystore = "~/keystore" # Path to the keystore file
# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters
# block-explorer = "Voyager" # Block explorer service used to display links to transaction details
# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer
111 changes: 111 additions & 0 deletions src/deployAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { RpcProvider, Account, stark, ec, hash, CallData, constants } from "starknet";
import * as dotenv from "dotenv";

dotenv.config();

// ─── DEPLOY ACCOUNT ON SEPOLIA ───────────────────────────────────────────────
// This script deploys an OpenZeppelin account contract using your private key.
// It uses STRK for gas fees (V3 transaction).

const PROVIDER = new RpcProvider({ nodeUrl: process.env.STARKNET_RPC_URL! });

// OpenZeppelin Account class hash on Sepolia
// This is the standard OZ Account v0.14.0 class hash
const OZ_ACCOUNT_CLASS_HASH = "0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f";

async function deployAccount() {
const privateKey = process.env.PRIVATE_KEY!;

// Derive public key from private key
const publicKey = ec.starkCurve.getStarkKey(privateKey);
console.log(`🔑 Public Key: ${publicKey}`);

// Compute the expected account address
const constructorCalldata = CallData.compile({ public_key: publicKey });
const computedAddress = hash.calculateContractAddressFromHash(
publicKey, // salt
OZ_ACCOUNT_CLASS_HASH,
constructorCalldata,
0 // deployer address (0 = not deployed via factory)
);
console.log(`📍 Computed Address: ${computedAddress}`);
console.log(`📍 Your .env Address: ${process.env.WALLET_ADDRESS}`);

// Check if the computed address matches your .env address
const envAddr = BigInt(process.env.WALLET_ADDRESS!);
const compAddr = BigInt(computedAddress);

if (envAddr !== compAddr) {
console.log(`\n⚠️ Address mismatch! Your wallet was likely created with a different class hash.`);
console.log(` Computed: ${computedAddress}`);
console.log(` .env: ${process.env.WALLET_ADDRESS}`);
console.log(`\n Trying alternative class hashes...`);

// Try common alternative class hashes
const alternatives = [
{ name: "OZ Account v0.8.1", hash: "0x05400e90f7b74d3fefba034769e661802e4f8f2ab0efbb1a0bd1dc3b82b48e5e" },
{ name: "OZ Account v0.9.0", hash: "0x01a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003" },
{ name: "OZ Account v0.11.0", hash: "0x04c6d6cf894f8bc96bb9c525e6853e5483177841f7388f74a46cfda6f028c755" },
{ name: "OZ Account v0.13.0", hash: "0x00e2eb8f5672af4e6a4e8a8f1b44989685e668489b0a25437733756c5a34a1d6" },
{ name: "Argent Account", hash: "0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f" },
{ name: "Braavos Account", hash: "0x00816dd0297efc55dc1e7559020a3a825e81ef734b558f03c83325d4da7e6253" },
];

for (const alt of alternatives) {
const altAddr = hash.calculateContractAddressFromHash(
publicKey,
alt.hash,
CallData.compile({ public_key: publicKey }),
0
);
if (BigInt(altAddr) === envAddr) {
console.log(`\n✅ Match found: ${alt.name} (${alt.hash})`);
console.log(` Use this class hash to deploy.`);

// Deploy with this class hash
await doDeployAccount(privateKey, publicKey, alt.hash, altAddr);
return;
}
}

console.log(`\n❌ Could not match your address to any known class hash.`);
console.log(` Your account may have been created with a custom salt or class hash.`);
console.log(` Please check which wallet (ArgentX, Braavos, etc.) generated this address.`);
return;
}

await doDeployAccount(privateKey, publicKey, OZ_ACCOUNT_CLASS_HASH, computedAddress);
}

async function doDeployAccount(privateKey: string, publicKey: string, classHash: string, address: string) {
console.log(`\n🚀 Deploying account at ${address}...`);

const account = new Account({ provider: PROVIDER, address, signer: privateKey });

const constructorCalldata = CallData.compile({ public_key: publicKey });

try {
const deployResponse = await account.deployAccount({
classHash: classHash,
constructorCalldata: constructorCalldata,
addressSalt: publicKey,
});

console.log(`📝 Deploy TX Hash: ${deployResponse.transaction_hash}`);
console.log(`⏳ Waiting for confirmation...`);

await PROVIDER.waitForTransaction(deployResponse.transaction_hash);
console.log(`✅ Account deployed successfully!`);
console.log(` Address: ${deployResponse.contract_address}`);
} catch (err: any) {
console.error(`❌ Deployment failed: ${err.message}`);

if (err.message.includes("Contract not found") || err.message.includes("Invalid block")) {
console.log(`\n💡 Tip: The account may need ETH or STRK for deployment gas.`);
console.log(` Your balance: 800 STRK, 0 ETH`);
console.log(` Try getting some Sepolia ETH from a faucet first.`);
}
}
}

deployAccount().catch(console.error);
94 changes: 94 additions & 0 deletions src/deployArgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { RpcProvider, Account, ec, hash, CallData, num } from "starknet";
import * as dotenv from "dotenv";

dotenv.config();

const PROVIDER = new RpcProvider({ nodeUrl: process.env.STARKNET_RPC_URL! });
const TARGET_ADDRESS = BigInt(process.env.WALLET_ADDRESS!);

async function findAndDeploy() {
const privateKey = process.env.PRIVATE_KEY!;
const publicKey = ec.starkCurve.getStarkKey(privateKey);
console.log(`🔑 Public Key: ${publicKey}`);
console.log(`🎯 Target Address: ${process.env.WALLET_ADDRESS}\n`);

// ArgentX class hashes (Sepolia + Mainnet, various versions)
const argentClassHashes = [
{ name: "Argent v0.4.0", hash: "0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f" },
{ name: "Argent v0.3.1", hash: "0x029927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b" },
{ name: "Argent v0.3.0", hash: "0x01a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003" },
{ name: "Argent v0.3.1 alt", hash: "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f" },
{ name: "Argent Sepolia latest", hash: "0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f" },
{ name: "Argent Cairo 1.0 v1", hash: "0x01148c31dfa5c4708a4e9cf1f2d3b26e8812c177bda19150757ca3ff74a4e3a0" },
{ name: "Argent Cairo 1.0 v2", hash: "0x023371b227eaecd8e8920cd429d2cd0f3fee6abaacca08d3ab82a7cdd", hash2: "0x05400e90f7b74d3fefba034769e661802e4f8f2ab0efbb1a0bd1dc3b82b48e5e" },
];

// Constructor formats to try
const constructorFormats = [
{ name: "(owner, guardian=0)", build: () => CallData.compile({ owner: publicKey, guardian: "0" }) },
{ name: "(signer, guardian=0)", build: () => CallData.compile({ signer: publicKey, guardian: "0" }) },
{ name: "(public_key)", build: () => CallData.compile({ public_key: publicKey }) },
{ name: "[publicKey, 0]", build: () => [publicKey, "0"] },
{ name: "[publicKey]", build: () => [publicKey] },
];

// Salt options
const salts = [
{ name: "publicKey", value: publicKey },
{ name: "0", value: "0" },
];

console.log(`🔍 Trying ${argentClassHashes.length} class hashes × ${constructorFormats.length} constructors × ${salts.length} salts...\n`);

for (const ch of argentClassHashes) {
for (const cf of constructorFormats) {
for (const salt of salts) {
try {
const calldata = cf.build();
const computed = hash.calculateContractAddressFromHash(
salt.value,
ch.hash,
calldata,
0
);

if (BigInt(computed) === TARGET_ADDRESS) {
console.log(`✅ MATCH FOUND!`);
console.log(` Class Hash: ${ch.name} (${ch.hash})`);
console.log(` Constructor: ${cf.name}`);
console.log(` Salt: ${salt.name}`);
console.log(` Address: ${computed}\n`);

// Now deploy
console.log(`🚀 Deploying account...`);
const account = new Account({ provider: PROVIDER, address: computed, signer: privateKey });

const deployResponse = await account.deployAccount({
classHash: ch.hash,
constructorCalldata: calldata,
addressSalt: salt.value,
});

console.log(`📝 TX Hash: ${deployResponse.transaction_hash}`);
console.log(`⏳ Waiting for confirmation...`);
await PROVIDER.waitForTransaction(deployResponse.transaction_hash);
console.log(`✅ Account deployed successfully at ${deployResponse.contract_address}!`);
return;
}
} catch {
// Skip invalid combinations silently
}
}
}
}

console.log(`❌ No match found with standard ArgentX class hashes.`);
console.log(`\n💡 Your best options:`);
console.log(` 1. Open Ready Wallet (ArgentX) in your browser`);
console.log(` 2. Make ANY transaction from it (even a 0 STRK transfer to yourself)`);
console.log(` 3. This will auto-deploy the account contract`);
console.log(` 4. Then the transfer agent script will work!\n`);
console.log(` OR: Use 'sncast account create' to make a new compatible wallet.`);
}

findAndDeploy().catch(console.error);
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { runTransferAgent } from "./transferAgent";
import * as dotenv from "dotenv";

dotenv.config();

runTransferAgent({
tokenAddress: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", // STRK on StarkNet
recipient: process.env.RECIPIENT_ADDRESS!,
transferAmount: BigInt("1000000000000000"), // 0.001 ETH in wei
transferThreshold: BigInt("5000000000000000"), // only transfer if balance > 0.005 ETH
alertThreshold: BigInt("2000000000000000"), // alert if balance < 0.002 ETH
});
Loading