Order Request - Go

Go examples use go-ethereum. See also https://goethereumbook.org/en/ for usage.

Full example script is available request_order.go.

Steps

  1. Derive an account from the mnemonic using go-ethereum-hdwallet or other provider.

  2. Make sure that account has ETH on Arbitrum to pay for transactions and USDC/USDT if placing buy orders.

  3. Place an order request with our smart contracts, including permitting our contracts to spend your USDC using go-ethereum.

  4. After to request transaction is submitted, get the order ID from the emitted OrderRequested event. Example coming soon.

  5. Continue to listen to events emitted by the order processor (https://goethereumbook.org/en/events/) to detect fills and cancels. Query the order processor for order state. Example coming soon.

Direct Orders

This example submits a BUY order request to the OrderProcessor contract. This process involves two method calls bundled together into one transaction using the OrderProcessor's multicall utility.

The first step is to determine the fees to add to the desired order amount.

// ------------------ Configure Order ------------------

// Set order amount (10 USDC)
orderAmount := new(big.Int).Mul(big.NewInt(10), big.NewInt(1e6))
fmt.Println("Order Amount:", orderAmount.String())
// Set buy or sell (false = buy, true = sell)
sellOrder := false
// Set order type (0 = market, 1 = limit)
orderType := uint8(0)

// Check the order decimals does not exceed max decimals
// Applicable to sell and limit orders only
if sellOrder || orderType == 1 {
	// Call the 'maxOrderDecimals' method on the contract to get max order decimals
	var maxDecimalsTxResult []interface{}
	maxDecimals := new(big.Int)
	maxDecimalsTxResult = append(maxDecimalsTxResult, maxDecimals)
	err = processorContract.Call(&bind.CallOpts{}, &maxDecimalsTxResult, "maxOrderDecimals")
	if err != nil {
		log.Fatalf("Failed to call getOrderDecimals function: %v", err)
	}
	fmt.Println("Order Decimals:", maxDecimals)

	// Call 'decimals' method on the asset token contract to get token decimals
	var assetTokenDecimalsTxResult []interface{}
	assetTokenDecimals := new(big.Int)
	assetTokenDecimalsTxResult = append(assetTokenDecimalsTxResult, assetTokenDecimals)
	assetTokenContract := bind.NewBoundContract(common.HexToAddress(AssetToken), eip2612Abi, client, client, client)
	err = assetTokenContract.Call(&bind.CallOpts{}, &assetTokenDecimalsTxResult, "decimals")
	if err != nil {
		log.Fatalf("Failed to call decimals function: %v", err)
	}
	fmt.Println("Asset Token Decimals:", assetTokenDecimals)

	// Calculate the allowable decimals
	allowableDecimals := new(big.Int).Sub(assetTokenDecimals, maxDecimals)
	if new(big.Int).Mod(orderAmount, allowableDecimals) != big.NewInt(0) {
		log.Fatalf("Order amount exceeds max alloeable decimals: %v", allowableDecimals)
	}
}

// Call the 'estimateTotalFeesForOrder' method on the contract to get total fees
var feeTxResult []interface{}
fees := new(OrderFee)
feeTxResult = append(feeTxResult, fees)
paymentTokenAddr := common.HexToAddress(PaymentTokenAddress)
err = processorContract.Call(&bind.CallOpts{}, &feeTxResult, "estimateTotalFeesForOrder", account.Address, false, paymentTokenAddr, orderAmount)
if err != nil {
	log.Fatalf("Failed to call estimateTotalFeesForOrder function: %v", err)
}
fmt.Println("processor fees:", fees.TotalFee)

// Calculate the total amount to spend considering fee rates
totalSpendAmount := new(big.Int).Add(orderAmount, fees.TotalFee)
fmt.Println("Total Spend Amount:", totalSpendAmount.String())

The first method call - selfPermit - gives the OrderProcessor permission to spend the payment token by pulling the order amount + fees from the user's account.

// ------------------ Configure Permit ------------------

// Call the 'name' method on the payment token contract to get token name
paymentTokenContract := bind.NewBoundContract(paymentTokenAddr, eip2612Abi, client, client, client)
var nameTxResult []interface{}
name := new(NameData)
nameTxResult = append(nameTxResult, name)
err = paymentTokenContract.Call(&bind.CallOpts{}, &nameTxResult, "name")
if err != nil {
	log.Fatalf("Failed to call name function: %v", err)
}
fmt.Println("Name:", name.Name)
var nonceTxResult []interface{}
nonce := new(NonceData)
nonceTxResult = append(nonceTxResult, nonce)
err = paymentTokenContract.Call(&bind.CallOpts{}, &nonceTxResult, "nonces", account.Address)
if err != nil {
	log.Fatalf("Failed to call nonces function: %v", err)
}
fmt.Println("Nonce:", nonce.Nonce)

// Fetch the current block number & its details to derive a deadline for the transaction
blockNumber, err := client.BlockNumber(context.Background())
if err != nil {
	log.Fatalf("Failed to get block number: %v", err)
}
block, err := client.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber)))
if err != nil {
	log.Fatalf("Failed to get block details: %v", err)
}
deadline := block.Time() + 300
deadlineBigInt := new(big.Int).SetUint64(deadline)
fmt.Println("Deadline:", deadline)

// Create the domain struct based on EIP712 requirements
domainStruct := apitypes.TypedDataDomain{
	Name:              name.Name,
	Version:           "1", // Version may be different in the wild
	ChainId:           math.NewHexOrDecimal256(chainID.Int64()),
	VerifyingContract: PaymentTokenAddress,
}

// Create the message struct for hashing
permitDataStruct := map[string]interface{}{
	"owner":    account.Address.String(),
	"spender":  processorAddress.String(),
	"value":    totalSpendAmount,
	"nonce":    nonce.Nonce,
	"deadline": deadlineBigInt,
}

// Define the DataTypes using previously defined types and the constructed structs
DataTypes := apitypes.TypedData{
	Types:       permitTypes, // This should be defined elsewhere (or passed as an argument if dynamic)
	PrimaryType: "Permit",
	Domain:      domainStruct,
	Message:     permitDataStruct,
}

// Hash the domain struct
domainHash, err := DataTypes.HashStruct("EIP712Domain", domainStruct.Map())
if err != nil {
	log.Fatalf("Failed to create EIP-712 domain hash: %v", err)
}
fmt.Println("Domain Separator: ", domainHash.String())

// Hash the message struct
permitTypeHash := DataTypes.TypeHash("Permit")
fmt.Println("Permit Type Hash: ", permitTypeHash.String())
messageHash, err := DataTypes.HashStruct("Permit", permitDataStruct)
if err != nil {
	log.Fatalf("Failed to create EIP-712 hash: %v", err)
}
fmt.Println("Permit Message Hash: ", messageHash.String())

// Combine and hash the domain and message hashes according to EIP-712
typedHash := crypto.Keccak256(append([]byte("\x19\x01"), append(domainHash, messageHash...)...))

// Sign the hash and construct R, S and V components of the signature
signature, err := crypto.Sign(typedHash, privateKey)
if err != nil {
	log.Fatalf("Failed to sign EIP-712 hash: %v", err)
}
if signature[64] < 27 {
	signature[64] += 27
}
fmt.Printf("EIP-712 Signature: 0x%x\n", hex.EncodeToString(signature))

r := signature[:32]
s := signature[32:64]
v := signature[64]

fmt.Printf("R: 0x%x\n", r)
fmt.Printf("S: 0x%x\n", s)
fmt.Printf("V: %d\n", v)

// Constructing function data for selfPermit
var rArray, sArray [32]byte
copy(rArray[:], r)
copy(sArray[:], s)

selfPermitData, err := processorAbi.Pack(
	"selfPermit",
	paymentTokenAddr,
	account.Address,
	totalSpendAmount,
	deadlineBigInt,
	v,
	rArray,
	sArray,
)
if err != nil {
	log.Fatalf("Failed to encode selfPermit function data: %v", err)
}

The second method call - requestOrder - submits the order request to be filled by the protocol. This is submitted with the spending permit.

// ------------------ Submit Order ------------------

// Constructing function data for requestOrder
order := OrderStruct{
	Recipient:            account.Address,
	AssetToken:           common.HexToAddress(AssetToken),
	PaymentToken:         paymentTokenAddr,
	Sell:                 sellOrder,
	OrderType:            orderType,
	AssetTokenQuantity:   big.NewInt(0),
	PaymentTokenQuantity: orderAmount,
	Price:                big.NewInt(0),
	Tif:                  1,
	SplitRecipient:       common.BigToAddress(big.NewInt(0)),
	SplitAmount:          big.NewInt(0),
}

requestOrderData, err := processorAbi.Pack(
	"requestOrder",
	order,
)
if err != nil {
	log.Fatalf("Failed to encode requestOrder function data: %v", err)
}

// Multicall - executing multiple transactions in one call
multicallArgs := [][]byte{
	selfPermitData,
	requestOrderData,
}

// Estimate gas limit for the transaction
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
	log.Fatalf("Failed to suggest gas price: %v", err)
}

opts := &bind.TransactOpts{
	From:     account.Address,
	Signer:   signer.Signer,
	GasLimit: 6721975, // Could be replaced with EstimateGas
	GasPrice: gasPrice,
	Value:    big.NewInt(0),
}

// Submitting the transaction and waiting for it to be mined
tx, err := processorContract.Transact(opts, "multicall", multicallArgs)
if err != nil {
	log.Fatalf("Failed to submit multicall transaction: %v", err)
}

// Verifying transaction status and printing result
receipt, err := bind.WaitMined(context.Background(), client, tx)
if err != nil {
	log.Fatalf("Failed to get transaction receipt: %v", err)
}

if receipt.Status == 0 {
	log.Fatalf("Transaction failed with hash: %s\n", tx.Hash().Hex())
} else {
	fmt.Printf("Transaction successful with hash: %s\n", tx.Hash().Hex())
}

Once the transaction is mined, the event logs from resulting transaction receipt can be unpacked to obtain the order recipient's order index. The recipient's order index can be used to query the BuyProcessor for the current state of the order or look up the event history for that order.

Last updated