diff --git a/account/account.go b/account/account.go index 2dc00bc2..71f1d4f7 100644 --- a/account/account.go +++ b/account/account.go @@ -473,7 +473,7 @@ func (account *Account) BlockNumber(ctx context.Context) (uint64, error) { // - blockID: The rpc.BlockID object representing the block. // Returns: // - uint64: the number of transactions in the block -// - error: an error, if any +// - error: an error, if any func (account *Account) BlockTransactionCount(ctx context.Context, blockID rpc.BlockID) (uint64, error) { return account.provider.BlockTransactionCount(ctx, blockID) } @@ -530,12 +530,12 @@ func (account *Account) ChainID(ctx context.Context) (string, error) { // // Parameters: // - ctx: The context.Context -// - blockID: The rpc.BlockID +// - blockID: The rpc.BlockID // - classHash: The `*felt.Felt` // Returns: -// - *rpc.ClassOutput: The rpc.ClassOutput (the class output could be a DeprecatedContractClass +// - *rpc.ClassOutput: The rpc.ClassOutput (the class output could be a DeprecatedContractClass // or just a Contract class depending on the contract version) -// - error: An error if any occurred. +// - error: An error if any occurred. func (account *Account) Class(ctx context.Context, blockID rpc.BlockID, classHash *felt.Felt) (rpc.ClassOutput, error) { return account.provider.Class(ctx, blockID, classHash) } @@ -546,9 +546,9 @@ func (account *Account) Class(ctx context.Context, blockID rpc.BlockID, classHas // - blockID: The rpc.BlockID object representing the block ID. // - contractAddress: The felt.Felt object representing the contract address. // Returns: -// - *rpc.ClassOutput: The rpc.ClassOutput object (the class output could be a DeprecatedContractClass +// - *rpc.ClassOutput: The rpc.ClassOutput object (the class output could be a DeprecatedContractClass // or just a Contract class depending on the contract version) -// - error: An error if any occurred. +// - error: An error if any occurred. func (account *Account) ClassAt(ctx context.Context, blockID rpc.BlockID, contractAddress *felt.Felt) (rpc.ClassOutput, error) { return account.provider.ClassAt(ctx, blockID, contractAddress) } @@ -618,7 +618,7 @@ func (account *Account) Nonce(ctx context.Context, blockID rpc.BlockID, contract } // SimulateTransactions simulates transactions using the provided context -// Parameters: +// Parameters: // - ctx: The context.Context object // - blockID: The rpc.BlockID object for the block referencing the state or call the transactions are on // - txns: The slice of rpc.Transaction objects representing the transactions to simulate @@ -707,7 +707,7 @@ func (account *Account) TransactionReceipt(ctx context.Context, transactionHash // Parameters: // - ctx: The context.Context object for the request. // - transactionHash: The transaction hash for which the transaction trace is to be retrieved. -// Returns: +// Returns: // - rpc.TxnTrace: The rpc.TxnTrace object representing the transaction trace, and an error if any. func (account *Account) TraceTransaction(ctx context.Context, transactionHash *felt.Felt) (rpc.TxnTrace, error) { return account.provider.TraceTransaction(ctx, transactionHash) @@ -824,3 +824,55 @@ func FmtCalldataCairo2(fnCalls []rpc.FunctionCall) []*felt.Felt { return execCallData } + +type DeployOptions struct { + ClassHash *felt.Felt // ClassHash is the unique identifier of the account class in the network. + MaxFee *felt.Felt // MaxFee represents the maximum fee the user is willing to pay for the deployment transaction. + DeployWaitTime time.Duration // DeployWaitTime specifies the duration to wait for the deployment transaction to be processed. + ConstructorCalldata []*felt.Felt // ConstructorCalldata contains the calldata to be passed to the constructor of the account contract upon deployment. +} + +func (account *Account) CreateAndExecuteAddDeployAccount(options DeployOptions) (*rpc.AddDeployAccountTransactionResponse, error) { + + pub, err := utils.HexToFelt(account.publicKey) + if err != nil { + return nil, err + } + + // Create transaction data + tx := rpc.DeployAccountTxn{ + Nonce: &felt.Zero, + MaxFee: options.MaxFee, + Type: rpc.TransactionType_DeployAccount, + Version: rpc.TransactionV1, + Signature: []*felt.Felt{}, + ClassHash: options.ClassHash, + ContractAddressSalt: pub, + ConstructorCalldata: options.ConstructorCalldata, + } + + precomputedAddress, err := account.PrecomputeAddress(&felt.Zero, pub, options.ClassHash, tx.ConstructorCalldata) + if err != nil { + return nil, err + } + + err = account.SignDeployAccountTransaction(context.Background(), &tx, precomputedAddress) + if err != nil { + return nil, err + } + + resp, err := account.AddDeployAccountTransaction(context.Background(), rpc.BroadcastDeployAccountTxn{ + DeployAccountTxn: tx, + }) + + if err != nil { + return nil, err + } + + _, err = account.WaitForTransactionReceipt(context.Background(), resp.TransactionHash, options.DeployWaitTime) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/account/account_test.go b/account/account_test.go index 0c80c60e..9129409e 100644 --- a/account/account_test.go +++ b/account/account_test.go @@ -41,7 +41,8 @@ var ( // Parameters: // - m: is the test main // Returns: -// none +// +// none func TestMain(m *testing.M) { flag.StringVar(&testEnv, "env", "mock", "set the test environment") flag.Parse() @@ -60,11 +61,13 @@ func TestMain(m *testing.M) { // of the transaction hash. Each test case consists of the expected hash, a flag // indicating whether the KeyStore should be set, account address, public key, // private key, chain ID, function call, and transaction details. -// +// // Parameters: // - t: The testing.T object for running the test +// // Returns: -// none +// +// none func TestTransactionHashInvoke(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -184,11 +187,12 @@ func TestTransactionHashInvoke(t *testing.T) { // // It tests the FmtCallData function by providing different test sets // and comparing the output with the expected call data. -// +// // Parameters: // - t: The testing.T instance for running the test // Return: -// none +// +// none func TestFmtCallData(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -253,7 +257,8 @@ func TestFmtCallData(t *testing.T) { // Parameters: // - t: The testing.T instance for running the test // Return: -// none +// +// none func TestChainIdMOCK(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -296,7 +301,8 @@ func TestChainIdMOCK(t *testing.T) { // Parameters: // - t: The testing.T instance for running the test // Return: -// none +// +// none func TestChainId(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -344,7 +350,8 @@ func TestChainId(t *testing.T) { // Parameters: // - t: The testing.T instance for running the test // Returns: -// none +// +// none func TestSignMOCK(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -404,7 +411,8 @@ func TestSignMOCK(t *testing.T) { // Parameters: // - t: The testing.T instance for running the test // Returns: -// none +// +// none func TestAddInvoke(t *testing.T) { type testSetType struct { @@ -582,9 +590,11 @@ func TestAddInvoke(t *testing.T) { // response is not nil. // // Parameters: -// - t: is the testing framework +// - t: is the testing framework +// // Returns: -// none +// +// none func TestAddDeployAccountDevnet(t *testing.T) { if testEnv != "devnet" { t.Skip("Skipping test as it requires a devnet environment") @@ -640,9 +650,11 @@ func TestAddDeployAccountDevnet(t *testing.T) { // Finally, it verifies that the calculated hash matches the expected hash. // // Parameters: -// - t: is the testing framework +// - t: is the testing framework +// // Returns: -// none +// +// none func TestTransactionHashDeployAccountTestnet(t *testing.T) { if testEnv != "testnet" { @@ -709,7 +721,8 @@ func TestTransactionHashDeployAccountTestnet(t *testing.T) { // Parameters: // - t: reference to the testing.T object // Returns: -// none +// +// none func TestTransactionHashDeclare(t *testing.T) { // https://goerli.voyager.online/tx/0x4e0519272438a3ae0d0fca776136e2bb6fcd5d3b2af47e53575c5874ccfce92 if testEnv != "testnet" { @@ -751,7 +764,8 @@ func TestTransactionHashDeclare(t *testing.T) { // Parameters: // - t: The testing.T object for test assertions and logging // Returns: -// none +// +// none func TestWaitForTransactionReceiptMOCK(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) @@ -833,7 +847,8 @@ func TestWaitForTransactionReceiptMOCK(t *testing.T) { // Parameters: // - t: The testing.T instance for running the test // Returns: -// none +// +// none func TestWaitForTransactionReceipt(t *testing.T) { if testEnv != "devnet" { t.Skip("Skipping test as it requires a devnet environment") @@ -882,9 +897,11 @@ func TestWaitForTransactionReceipt(t *testing.T) { // It asserts that the expected hash and error values are returned for each test set. // // Parameters: -// - t: The testing.T instance for running the test +// - t: The testing.T instance for running the test +// // Returns: -// none +// +// none func TestAddDeclareTxn(t *testing.T) { // https://goerli.voyager.online/tx/0x76af2faec46130ffad1ab2f615ad16b30afcf49cfbd09f655a26e545b03a21d if testEnv != "testnet" { @@ -955,6 +972,49 @@ func TestAddDeclareTxn(t *testing.T) { } } +func TestCreateAndExecuteAddDeployAccount(t *testing.T) { + if testEnv != "devnet" { + t.Skip("Skipping test as it requires a devnet environment") + } + client, err := rpc.NewClient(base + "/rpc") + require.NoError(t, err, "Error in rpc.NewClient") + provider := rpc.NewProvider(client) + + devnet, acnts, err := newDevnet(t, base) + require.NoError(t, err, "Error setting up Devnet") + fakeUser := acnts[0] + fakeUserAddr := utils.TestHexToFelt(t, fakeUser.Address) + fakeUserPub := utils.TestHexToFelt(t, fakeUser.PublicKey) + + // Set up ks + ks := account.NewMemKeystore() + fakePrivKeyBI, ok := new(big.Int).SetString(fakeUser.PrivateKey, 0) + require.True(t, ok) + ks.Put(fakeUser.PublicKey, fakePrivKeyBI) + + acnt, err := account.NewAccount(provider, fakeUserAddr, fakeUser.PublicKey, ks) + require.NoError(t, err) + + classHash := utils.TestHexToFelt(t, "0x7b3e05f48f0c69e4a65ce5e076a66271a527aff2c34ce1083ec6e1526997a69") // preDeployed classhash + require.NoError(t, err) + + precomputedAddress, err := acnt.PrecomputeAddress(&felt.Zero, fakeUserPub, classHash, []*felt.Felt{fakeUserPub}) + + _, err = devnet.Mint(precomputedAddress, new(big.Int).SetUint64(10000000000000000000)) + + require.NoError(t, err) + + deployOptions := account.DeployOptions{ + ClassHash: classHash, + MaxFee: new(felt.Felt).SetUint64(4724395326064), + DeployWaitTime: 2 * time.Second, + ConstructorCalldata: []*felt.Felt{fakeUserPub}, + } + resp, err := acnt.CreateAndExecuteAddDeployAccount(deployOptions) + require.NoError(t, err, "DeployAccount gave an Error") + require.NotNil(t, resp, "DeployAccount resp not nil") +} + // newDevnet creates a new devnet with the given URL. // // Parameters: @@ -964,6 +1024,7 @@ func TestAddDeclareTxn(t *testing.T) { // - *devnet.DevNet: a pointer to a devnet object // - []devnet.TestAccount: a slice of test accounts // - error: an error, if any + func newDevnet(t *testing.T, url string) (*devnet.DevNet, []devnet.TestAccount, error) { devnet := devnet.NewDevNet(url) acnts, err := devnet.Accounts() diff --git a/examples/createAndDeployAccount/.env.template b/examples/createAndDeployAccount/.env.template new file mode 100644 index 00000000..70f7e092 --- /dev/null +++ b/examples/createAndDeployAccount/.env.template @@ -0,0 +1,2 @@ +# use this variable to change the RPC base URL +#INTEGRATION_BASE=http_insert_end_point diff --git a/examples/createAndDeployAccount/README.md b/examples/createAndDeployAccount/README.md new file mode 100644 index 00000000..047ba153 --- /dev/null +++ b/examples/createAndDeployAccount/README.md @@ -0,0 +1,12 @@ +This example uses a pre-existing contract on the goerli network to deploy a new account contract. To successfully run this example, you will need: 1) a goerli endpoint, and 2) to fund the precomputed address. + +Steps: +1. Rename the ".env.template" file to ".env.testnet" +2. Uncomment, and assign your testnet endpoint to the "INTEGRATION_BASE" variable +3. Execute `go mod tidy` (make sure you are in the "deployAccount" folder) +4. Execute `go run main.go` +5. Fund the precomputed address using a starknet faucet, eg https://faucet.goerli.starknet.io/ +6. Press any key, then enter + +At this point your account should be deployed on testnet, and you can use a block explorer like [Voyager](https://voyager.online/) to view your transaction using the transaction hash. + diff --git a/examples/createAndDeployAccount/go.mod b/examples/createAndDeployAccount/go.mod new file mode 100644 index 00000000..d2d1fc18 --- /dev/null +++ b/examples/createAndDeployAccount/go.mod @@ -0,0 +1,38 @@ +module account + +go 1.21 + +require ( + github.com/NethermindEth/juno v0.3.1 + github.com/NethermindEth/starknet.go v0.2.1-0.20220620163912-1db2ca279608 + github.com/ethereum/go-ethereum v1.10.26 + github.com/joho/godotenv v1.4.0 +) + +replace github.com/NethermindEth/starknet.go => ../../ + +require ( + github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect + github.com/bits-and-blooms/bitset v1.7.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.11.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/stretchr/testify v1.8.1 // indirect + github.com/test-go/testify v1.1.4 // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.2.0 // indirect + golang.org/x/sys v0.3.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/examples/createAndDeployAccount/go.work b/examples/createAndDeployAccount/go.work new file mode 100644 index 00000000..01071242 --- /dev/null +++ b/examples/createAndDeployAccount/go.work @@ -0,0 +1,6 @@ +go 1.21 + +use ( + . + ../.. +) diff --git a/examples/createAndDeployAccount/main.go b/examples/createAndDeployAccount/main.go new file mode 100644 index 00000000..b255a21c --- /dev/null +++ b/examples/createAndDeployAccount/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "time" + + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/starknet.go/account" + "github.com/NethermindEth/starknet.go/rpc" + "github.com/NethermindEth/starknet.go/utils" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/joho/godotenv" +) + +var ( + network string = "testnet" + predeployedClassHash = "0x2794ce20e5f2ff0d40e632cb53845b9f4e526ebd8471983f7dbd355b721d5a" + accountAddress = "0xdeadbeef" +) + +// main, similar to the deployAccount example, initializes the client, generates random keys, and sets up a new account for StarkNet deployment. +// +// It starts by loading environment variables and connecting to the Ethereum RPC. Random keys are generated for account creation. The account address is converted to a felt type, and the account is initialized with these parameters. +// +// The function precomputes an address, asking the user to fund it. After user confirmation, it uses CreateAndExecuteAddDeployAccount for deployment, streamlining the process compared to the deployAccount example. +// +// Parameters: +// +// none +// +// Returns: +// +// none +func main() { + // Initialise the client. + godotenv.Load(fmt.Sprintf(".env.%s", network)) + base := os.Getenv("INTEGRATION_BASE") + c, err := ethrpc.DialContext(context.Background(), base) + if err != nil { + panic("You need to specify the testnet url in .env.testnet") + } + clientv02 := rpc.NewProvider(c) + + // Get random keys for test purposes + ks, pub, _ := account.GetRandomKeys() + + accountAddressFelt, err := new(felt.Felt).SetString(accountAddress) + if err != nil { + panic("Error casting accountAddress to felt") + } + + // Set up account + acnt, err := account.NewAccount(clientv02, accountAddressFelt, pub.String(), ks) + if err != nil { + panic(err) + } + + classHash, err := utils.HexToFelt(predeployedClassHash) + if err != nil { + panic(err) + } + + precomputedAddress, err := acnt.PrecomputeAddress(&felt.Zero, pub, classHash, []*felt.Felt{pub}) + + fmt.Printf("\nIn order to deploy your account (address %s), you need to fund the acccount (using a faucet), and then press `enter` to continue : \n", precomputedAddress.String()) + + reader := bufio.NewReader(os.Stdin) + _, err = reader.ReadString('\n') + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Println("Waiting for deployment") + deployOptions := account.DeployOptions{ + ClassHash: classHash, + MaxFee: new(felt.Felt).SetUint64(4724395326064), + DeployWaitTime: 2 * time.Second, + ConstructorCalldata: []*felt.Felt{pub}, + } + + // Deploy the account + resp, err := acnt.CreateAndExecuteAddDeployAccount(deployOptions) + + if err != nil { + panic(fmt.Sprint("Error returned from DeployAccount: ", err)) + } + fmt.Println("Deployed with response response:", resp) + +} diff --git a/examples/deployAccount/README.md b/examples/deployAccount/README.md index 67408e41..047ba153 100644 --- a/examples/deployAccount/README.md +++ b/examples/deployAccount/README.md @@ -1,4 +1,3 @@ - This example uses a pre-existing contract on the goerli network to deploy a new account contract. To successfully run this example, you will need: 1) a goerli endpoint, and 2) to fund the precomputed address. Steps: diff --git a/examples/deployAccount/main.go b/examples/deployAccount/main.go index 499e30c8..474d78bb 100644 --- a/examples/deployAccount/main.go +++ b/examples/deployAccount/main.go @@ -27,9 +27,12 @@ var ( // and finally sends the transaction to the network. // // Parameters: -// none +// +// none +// // Returns: -// none +// +// none func main() { // Initialise the client. godotenv.Load(fmt.Sprintf(".env.%s", network))