├── .gitignore ├── StarkNet.js 101.pdf ├── package.json ├── precalculate_address.js ├── README.md ├── deploy_account.js └── deploy_erc20.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | -------------------------------------------------------------------------------- /StarkNet.js 101.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xs34n/starknet.js-workshop/HEAD/StarkNet.js 101.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starknetjs-101", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "starknet": "^4.12.0" 14 | }, 15 | "type": "module" 16 | } 17 | -------------------------------------------------------------------------------- /precalculate_address.js: -------------------------------------------------------------------------------- 1 | import { 2 | hash, 3 | ec, 4 | stark 5 | } from "starknet"; 6 | 7 | 8 | ///// STEP 1 ///// 9 | 10 | // - Pre-calculate the address of the account contract 11 | // - You will send ETH to this address in order to pay for the fee for account deployment 12 | // - This is necessary because StarkNet doesn't allow free account deployments 13 | 14 | // Generate public and private key pair. 15 | const privateKey = stark.randomAddress(); // generate a random private key 16 | 17 | const starkKeyPair = ec.getKeyPair(privateKey); 18 | const starkKeyPub = ec.getStarkKey(starkKeyPair); 19 | 20 | console.log("Private key, copy it for later: ", privateKey); 21 | 22 | // class hash of ./Account.json. 23 | // Starknet.js currently doesn't have the functionality to calculate the class hash 24 | const OZContractClassHash = '0x058d97f7d76e78f44905cc30cb65b91ea49a4b908a76703c54197bca90f81773'; 25 | 26 | const precalculatedAddress = hash.calculateContractAddressFromHash( 27 | starkKeyPub, // salt 28 | OZContractClassHash, 29 | [starkKeyPub], 30 | 0 31 | ); 32 | 33 | console.log("pre-calculated address: ", precalculatedAddress); 34 | 35 | ///// STEP 2 ///// 36 | 37 | //////////////////////////////////////////////////////////////////////////////// 38 | // IMPORTANT: 39 | // you need to fund your newly calculated address before you can actually deploy the account. 40 | // 41 | // You can do so by using a faucet on the TESTNET: 42 | // https://faucet.goerli.starknet.io/ 43 | // 44 | // Or the DEVENT: 45 | // 46 | // curl -X POST http://127.0.0.1:5050/mint -d '{"address":"0x04a093c37ab61065d001550089b1089922212c60b34e662bb14f2f91faee2979","amount":50000000000000000000,"lite":true}' -H "Content-Type:application/json" 47 | // {"new_balance":50000000000000000000,"tx_hash":null,"unit":"wei"} 48 | // 49 | // ...where the address is from the newly precalculatedAddress 50 | //////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Account and ERC20 Demo for "Encode Starknet.js 101 Workshop" 2 | 3 | Install latest LTS version of node (at the time of writing, version is v16.16.0) 4 | 5 | Run `npm install` in this directory. 6 | 7 | In your own code please run `npm install starknet@next` for the latest version of starknet.js. 8 | 9 | The Account contract used in this workshop is made by [OpenZeppelin](https://github.com/OpenZeppelin/cairo-contracts): **contract version 0.5.1** 10 | 11 | 12 | **Argent accounts example**: 13 | For an example on how to do this with Argent's account, take a look at this [tutorial](https://github.com/0xs34n/starknet.js-account). 14 | 15 | The compilation of Account.json was done by using Nile and these [instructions](https://github.com/OpenZeppelin/cairo-contracts#first-time). 16 | 17 | Class hash was obtained with the [Starkli](https://github.com/xJonathanLEI/starkli) tool. 18 | 19 | 20 | ## THEORY 21 | 22 | High level explanations from StarkWare: 23 | 24 | https://starkware.notion.site/Deploy-a-contract-and-an-account-on-StarkNet-ed2fd13301d2414e8223bb72bb90e386 25 | 26 | ## Start the demo: 27 | 28 | **NOTE**: this demo was done and tested with the local devnet, which we recommend to do also. 29 | 30 | 1. Install the devnet following the official [documentation](https://shard-labs.github.io/starknet-devnet/docs/intro) 31 | 2. Go to the devnet repo and start: 32 | `starknet-devnet --seed 0` -> `--seed 0` ensures the creation of same predeployed accounts each time 33 | 34 | ### 1. Precalculate Address + Send Funds 35 | 36 | `node precalculate_address.js` 37 | 38 | ### 2. Deploy Account 39 | 40 | `node deploy_account.js` 41 | 42 | **NOTE:** if you start like this, the workshop will run on the goerli testnet. 43 | 44 | To start with the **local devnet**: 45 | Go to the workshop repo and start like this: 46 | `STARKNET_PROVIDER_BASE_URL=http://127.0.0.1:5050/ node deploy_account.js` 47 | 48 | ### 3. Deploy ERC20 and interact 49 | 50 | **NOTE:** tested on devnet only 51 | 52 | `STARKNET_PROVIDER_BASE_URL=http://127.0.0.1:5050/ node deploy_erc20.js` 53 | 54 | --- 55 | 56 | ## Videos: 57 | This workshop, along with general info about starknet.js, is shown in videos below: 58 | 59 | - https://www.youtube.com/watch?v=gqj0ENOE0EE 60 | 61 | - https://youtu.be/6jGlDBRvckU?t=2167 62 | 63 | **Note:** these videos are outdated with regards to code, but still are nice introduction to Starknet.js 64 | 65 | ## Questions? 66 | 67 | Ask in #starknet-js channel in the [StarkNet Discord](https://discord.gg/C2JsG2j7Fs) 68 | 69 | DMs - 0xSean#1534 on Discord 70 | -------------------------------------------------------------------------------- /deploy_account.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { 4 | defaultProvider, 5 | ec, 6 | json, 7 | SequencerProvider, 8 | Account, 9 | } from "starknet"; 10 | 11 | // SETUP 12 | 13 | const provider = process.env.STARKNET_PROVIDER_BASE_URL === undefined ? 14 | defaultProvider : 15 | new SequencerProvider({ baseUrl: process.env.STARKNET_PROVIDER_BASE_URL }); 16 | 17 | console.log("Reading OpenZeppelin Account Contract..."); 18 | 19 | const compiledOZAccount = json.parse( 20 | fs.readFileSync("./Account.json").toString("ascii") 21 | ); 22 | 23 | // class hash of ./Account.json. 24 | // Starknet.js currently doesn't have the functionality to calculate the class hash 25 | const OZContractClassHash = '0x058d97f7d76e78f44905cc30cb65b91ea49a4b908a76703c54197bca90f81773'; 26 | 27 | /////////////////////////////////////////////////////////////////// 28 | // Since there are no Externally Owned Accounts (EOA) in StarkNet, 29 | // all Accounts in StarkNet are contracts. 30 | 31 | // Unlike in Ethereum where a account is created with a public and private key pair, 32 | // StarkNet Accounts are the only way to sign transactions and messages, and verify signatures. 33 | // Therefore a Account - Contract interface is needed. 34 | /////////////////////////////////////////////////////////////////// 35 | 36 | 37 | ///// STEP 3 - optional ///// 38 | // Declare account contract IF IT HAS NOT ALREADY BEEN DECLARED BEFORE 39 | 40 | // NOTE: This step will fail if you haven't sent funds to the predeployed address in STEP 1 41 | 42 | // We need to use an already deployed account in order to declare ours. 43 | // StarkNet will always have at least 1 already declared/deployed account for this purpose. 44 | // In our case we will use the devnet's predeployed OZ account, after you start the devnet with: `starknet-devnet --seed 0` 45 | 46 | const devnetPrivateKey = '0xe3e70682c2094cac629f6fbed82c07cd'; 47 | const devnetAccount0Address = '0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a'; 48 | const devnetKeyPair = ec.getKeyPair(devnetPrivateKey); 49 | 50 | const predeployedAccount = new Account(provider, devnetAccount0Address, devnetKeyPair); 51 | 52 | const declareTx = await predeployedAccount.declare({ 53 | classHash: OZContractClassHash, 54 | contract: compiledOZAccount 55 | }); 56 | 57 | await provider.waitForTransaction(declareTx.transaction_hash); 58 | 59 | console.log("account declared"); 60 | console.log(declareTx); 61 | 62 | 63 | ///// STEP 4 ///// 64 | // Deploy account contract 65 | // NOTE: This step will fail if you haven't sent funds to the predeployed address in STEP 1 66 | 67 | const precalculatedAddress = '0x21da1673e364bd2719563b2e211bcda6b57624291ab26e6e61a2d6dc2de3992'; // get from step 1 68 | const privateKey = '0x0325f5dbc170b650b91adb2fe151445584746d9a294c72e7429e01814d7a5961'; // get from step 1 69 | const starkKeyPair = ec.getKeyPair(privateKey); 70 | const starkKeyPub = ec.getStarkKey(starkKeyPair); 71 | 72 | const account = new Account(provider, precalculatedAddress, starkKeyPair); 73 | 74 | const accountResponse = await account.deployAccount({ 75 | classHash: OZContractClassHash, 76 | constructorCalldata: [starkKeyPub], 77 | contractAddress: precalculatedAddress, 78 | addressSalt: starkKeyPub 79 | }); 80 | 81 | await provider.waitForTransaction(accountResponse.transaction_hash); 82 | console.log(accountResponse); -------------------------------------------------------------------------------- /deploy_erc20.js: -------------------------------------------------------------------------------- 1 | /////////////////////////// 2 | // 3 | // ERC20 DEPLOYMENT AND INTERACTION 4 | // 5 | // The account we use here is already prefunded in the devnet. 6 | // 7 | // We shall deploy and call the OpenZeppelin ERC20 contract. 8 | // 9 | /////////////////////////// 10 | 11 | 12 | import fs from "fs"; 13 | 14 | // Install the latest version of starknet with npm install starknet@next and import starknet 15 | import { 16 | Account, 17 | defaultProvider, 18 | ec, 19 | json, 20 | SequencerProvider, 21 | Contract, 22 | stark, 23 | number, 24 | shortString 25 | } from "starknet"; 26 | 27 | // SETUP 28 | 29 | const provider = process.env.STARKNET_PROVIDER_BASE_URL === undefined ? 30 | defaultProvider : 31 | new SequencerProvider({ baseUrl: process.env.STARKNET_PROVIDER_BASE_URL }); 32 | 33 | console.log("Reading ERC20 Contract..."); 34 | const compiledErc20 = json.parse( 35 | fs.readFileSync("./ERC20.json").toString("ascii") 36 | ); 37 | 38 | // Note: cleanHex will be redundant with nevwer starknet.js version 39 | const cleanHex = (hex) => hex.toLowerCase().replace(/^(0x)0+/, '$1'); 40 | 41 | // devnet private key from Account #0 if generated with --seed 0 42 | const starkKeyPair = ec.getKeyPair("0xe3e70682c2094cac629f6fbed82c07cd"); 43 | const accountAddress = "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a"; 44 | 45 | const recieverAddress = '0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79'; 46 | 47 | // Starknet.js currently doesn't have the functionality to calculate the class hash 48 | const erc20ClassHash = '0x03f794a28472089a1a99b7969fc51cd5fbe22dd09e3f38d2bd6fa109cb3f4ecf'; 49 | 50 | const account = new Account( 51 | provider, 52 | accountAddress, 53 | starkKeyPair 54 | ); 55 | 56 | // 1. DECLARE CONTRACT 57 | 58 | const erc20DeclareResponse = await account.declare({ 59 | classHash: erc20ClassHash, 60 | contract: compiledErc20, 61 | }); 62 | 63 | await provider.waitForTransaction(erc20DeclareResponse.transaction_hash); 64 | 65 | console.log("erc20 contract declared"); 66 | console.log(erc20DeclareResponse); 67 | 68 | // 2. DEPLOY CONTRACT 69 | 70 | // Deploy an ERC20 contract and wait for it to be verified on StarkNet. 71 | console.log("Deployment Tx - ERC20 Contract to StarkNet..."); 72 | 73 | const salt = '900080545022'; // use some random salt 74 | 75 | const erc20Response = await account.deploy({ 76 | classHash: erc20ClassHash, 77 | constructorCalldata: stark.compileCalldata({ 78 | name: shortString.encodeShortString('TestToken'), 79 | symbol: shortString.encodeShortString('ERC20'), 80 | decimals: 18, 81 | initial_supply: ['1000'], 82 | recipient: account.address, 83 | }), 84 | salt, 85 | }); 86 | 87 | 88 | console.log("Waiting for Tx to be Accepted on Starknet - ERC20 Deployment..."); 89 | await provider.waitForTransaction(erc20Response.transaction_hash); 90 | 91 | const txReceipt = await provider.getTransactionReceipt(erc20Response.transaction_hash); 92 | 93 | 94 | /////////////////////////////// 95 | // Contract interaction 96 | /////////////////////////////// 97 | 98 | // Get the erc20 contract address 99 | const erc20Event = parseUDCEvent(txReceipt); 100 | console.log("ERC20 Address: ", erc20Event.address); 101 | 102 | const erc20Address = erc20Event.address 103 | 104 | // Create a new erc20 contract object 105 | const erc20 = new Contract(compiledErc20.abi, erc20Address, provider); 106 | 107 | erc20.connect(account); 108 | 109 | // OPTION 1 - call as contract object 110 | 111 | console.log(`Invoke Tx - Sending 10 tokens to ${recieverAddress}...`); 112 | const { transaction_hash: mintTxHash } = await erc20.transfer( 113 | recieverAddress, 114 | ['0', '10'], // send 10 tokens as Uint256 115 | ); 116 | 117 | // Wait for the invoke transaction to be accepted on StarkNet 118 | console.log(`Waiting for Tx to be Accepted on Starknet - Transfer...`); 119 | await provider.waitForTransaction(mintTxHash); 120 | 121 | console.log(`Calling StarkNet for account balance...`); 122 | const balanceBeforeTransfer = await erc20.balanceOf(account.address); 123 | 124 | console.log( 125 | `account Address ${account.address} has a balance of:`, 126 | number.toBN(balanceBeforeTransfer[0].high).toString() 127 | ); 128 | 129 | // OPTION 2 - call contract from Account 130 | 131 | //Execute tx transfer of 10 tokens 132 | console.log(`Invoke Tx - Transfer 10 tokens to ${recieverAddress}...`); 133 | const executeHash = await account.execute( 134 | { 135 | contractAddress: erc20Address, 136 | entrypoint: 'transfer', 137 | calldata: stark.compileCalldata({ 138 | recipient: recieverAddress, 139 | amount: ['10'] 140 | }) 141 | } 142 | ); 143 | 144 | console.log(`Waiting for Tx to be Accepted on Starknet - Transfer...`); 145 | await provider.waitForTransaction(executeHash.transaction_hash); 146 | 147 | 148 | // Check balances 149 | 150 | // Sender 151 | console.log(`Calling StarkNet for Sender account balance...`); 152 | const balanceAfterTransfer = await erc20.balanceOf(account.address); 153 | 154 | console.log( 155 | `account Sender ${account.address} has a balance of:`, 156 | number.toBN(balanceAfterTransfer[0].high).toString() 157 | ); 158 | 159 | // Reciever 160 | console.log(`Calling StarkNet for Reciever account balance...`); 161 | const recieverAfterTransfer = await erc20.balanceOf(recieverAddress); 162 | 163 | console.log( 164 | `account Reciever ${recieverAddress} has a balance of:`, 165 | number.toBN(recieverAfterTransfer[0].high).toString() 166 | ); 167 | 168 | 169 | // NOTE: parseUDCEvent will be redundant with nevwer starknet.js version 170 | 171 | function parseUDCEvent(txReceipt) { 172 | if (!txReceipt.events) { 173 | throw new Error('UDC emited event is empty'); 174 | } 175 | const event = txReceipt.events.find( 176 | (it) => cleanHex(it.from_address) === cleanHex('0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf') // UDC address 177 | ) || { 178 | data: [], 179 | }; 180 | return { 181 | transaction_hash: txReceipt.transaction_hash, 182 | contract_address: event.data[0], 183 | address: event.data[0], 184 | deployer: event.data[1], 185 | unique: event.data[2], 186 | classHash: event.data[3], 187 | calldata_len: event.data[4], 188 | calldata: event.data.slice(5, 5 + parseInt(event.data[4], 16)), 189 | salt: event.data[event.data.length - 1], 190 | }; 191 | } 192 | 193 | --------------------------------------------------------------------------------