Create Your First Wallet

Circle Programmable Wallets provide a comprehensive developer solution to storing, sending, and spending Web3 digital currencies and NFTs. You or your users can manage asset infrastructure. Circle provides a one-stop-shop experience with all the tools and services to handle the complex parts, including security, transaction monitoring, account recovery flows, and more.ย 

Note: The following guide uses the Polygon (MATIC) Testnet for illustration purposes. In production, you can create and use programmable wallets that support crypto tokens with the following blockchains and standards:

  • Ethereum (ETH), Polygon (MATIC), and Avalanche (AVAX), both Testnet and Mainnet
  • ERC-20 tokens
  • ERC-721 and ERC-1155 NFTs (non-fungible tokens)

๐Ÿ“˜

This guide features a Developer-Controlled wallet. Programmable Wallets also support a user-controlled wallet quickstart.

Prerequisites

View sequence diagram

Developer-Controlled Wallet Create Your First Wallet Sequence Diagram

1. Register an Entity Secret Ciphertext

a. Generate the Entity Secret

First, generate the Entity Secret and store it somewhere safe where your server-side app can access it. For testing purposes, you can store it in your environment variables. This will be used in the following steps to create the Entity Secret Ciphertext.

When you follow the provided methods below, you will receive a 32-byte string value similar to 7ae43b03d7e48795cbf39ddad2f58dc8e186eb3d2dab3a5ec5bb3b33946639a4.

openssl rand -hex 32
const crypto = require('crypto')
const secret = crypto.randomBytes(32).toString('hex')

console.log(secret)  
let array = new Uint8Array(32)
window.crypto.getRandomValues(array)
let secret = Array.from(array)
  .map((b) => b.toString(16).padStart(2, '0'))
  .join('')

console.log(secret)
import os

secret = os.urandom(32).hex()

print(secret)
package main

import (
	"crypto/rand"
	"fmt"
	"io"
)

func generateRandomHex() []byte {
	mainBuff := make([]byte, 32)
	_, err := io.ReadFull(rand.Reader, mainBuff)
	if err != nil {
		panic("reading from crypto/rand failed: " + err.Error())
	}
	return mainBuff
}

// The following sample codes generate a distinct hex encoded entity secret with each execution
// The generation of entity secret only need to be executed once unless you need to rotate entity secret.
func main() {
	entitySecret := generateRandomHex()
	fmt.Printf("Hex encoded entity secret: %x
", entitySecret)
}

๐Ÿšง

Remember to keep the Entity Secret safe. Securely store it, as you'll need it to create an Entity Secret Ciphertext in the following steps.

b. Fetch your Entity's Public Key

To proceed with the Entity Secret Ciphertext creation, the next essential element is your entity's public key. This public key plays an important role in generating the Entity Secret Ciphertext in the upcoming step. To obtain the required public key, you need to initiate a request to the GET /config/entity/publicKey API endpoint. Remember, this API endpoint can be accessed by providing your valid API key for authentication.

curl --request GET \
     --url 'https://api.circle.com/v1/w3s/config/entity/publicKey' \
     --header 'accept: application/json' \
     --header 'authorization: Bearer <API_KEY>'
{
  "data": {
    "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsL4dzMMQX8pYbiyj0g5H\nFGwdygAA2xDm2VquY8Sk0xlOC0yKr+rqUrqnZCj09vLuXbg1BreO/BP4F4GEIHgR\nBNT+o5Q8k0OqxLXmcm5sz6CPlsFCom+MiOj6s7RD0SXg91WF8MrN88GyN53xkemA\nOYU1AlIt4dVIrFyGY8aQ57sbWHyIjim+do1kBX+svIA/FLHG/sycoGiPU1E+Kydf\nlEDga4iR2DSbW6Zte9cGDg9Ivw/seNd0TLzJz6oC9XgSK5Et6/ZpOmqJgvISQ6rT\nK15DJ8EzIOzZZuEVOefgy1S7rLdSH7DexuR4W7T+KpP/f8Px0bxd4N6MT5V5kBYa\ngYHHIvqlJvXe5EzwidIWk1rg1X+YJt2M48h3Pr9HeECcmrnEYOgp32m/9lJ8vKp9\nhNh0rEKww/ULd1HqCEm/I0QGuji13XcGxVo5+7KCb/C76CNdW3pdRMn6fwFh4WVu\nu99iRc9OZhlkphysWm44hs1ZPpMCAkKttWjhnLZwIatN27x2JUqoCEUOho19iT+F\nwlPFA7E0Ju9Rqm68AkCXxHsJsAuGT8m6FLQZLHv4JyO/QEVzD7vY08A2I5dz1mVt\ngVam1/05Axju6poRomx/DUxiR0QH1+0Kg15+2A0fRkBggTTn7kvGsgz0cqk9cTm0\nEITpIVGcSGrVNRrmSye2OW0CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"
  }
}

c. Generate the Entity Secret Ciphertext

Once you have the public key, you'll use RSA encryption to secure your Entity Secret and generate the ciphertext. Immediately after, you'll transform this encrypted data into the Base64 format. The output Ciphertext will be exactly 684 characters long.

const forge = require('node-forge')

const entitySecret = forge.util.hexToBytes('YOUR_ENTITY_SECRET')
const publicKey = forge.pki.publicKeyFromPem('YOUR_PUBLIC_KEY')
const encryptedData = publicKey.encrypt(entitySecret, 'RSA-OAEP', {
  md: forge.md.sha256.create(),
  mgf1: {
    md: forge.md.sha256.create(),
  },
})

console.log(forge.util.encode64(encryptedData))
import base64
import codecs
# Installed by `pip install pycryptodome`
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256

# Paste your entity public key here.
public_key_string = 'PASTE_YOUR_PUBLIC_KEY_HERE'

# If you already have a hex encoded entity secret, you can paste it here. the length of the hex string should be 64.
hex_encoded_entity_secret = 'PASTE_YOUR_HEX_ENCODED_ENTITY_SECRET_KEY_HERE'

# The following sample codes generate a distinct entity secret ciphertext with each execution.
if __name__ == '__main__':
    entity_secret = bytes.fromhex(hex_encoded_entity_secret)

    if len(entity_secret) != 32:
        print("invalid entity secret")
        exit(1)

    public_key = RSA.importKey(public_key_string)

    # encrypt data by the public key
    cipher_rsa = PKCS1_OAEP.new(key=public_key, hashAlgo=SHA256)
    encrypted_data = cipher_rsa.encrypt(entity_secret)

    # encode to base64
    encrypted_data_base64 = base64.b64encode(encrypted_data)

    print("Hex encoded entity secret:", codecs.encode(entity_secret, 'hex').decode())
    print("Entity secret ciphertext:", encrypted_data_base64.decode())

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"fmt"
)

// Paste your entity public key here.
var publicKeyString = "PASTE_YOUR_PUBLIC_KEY_HERE"

// If you already have a hex encoded entity secret, you can paste it here. the length of the hex string should be 64.
var hexEncodedEntitySecret = "PASTE_YOUR_HEX_ENCODED_ENTITY_SECRET_KEY_HERE"

// The following sample codes generate a distinct entity secret ciphertext with each execution
func main() {
	entitySecret, err := hex.DecodeString(hexEncodedEntitySecret)
	if err != nil {
		panic(err)
	}
	if len(entitySecret) != 32 {
		panic("invalid entity secret")
	}
	pubKey, err := ParseRsaPublicKeyFromPem([]byte(publicKeyString))
	if err != nil {
		panic(err)
	}
	cipher, err := EncryptOAEP(pubKey, entitySecret)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Hex encoded entity secret: %x
", entitySecret)
	fmt.Printf("Entity secret ciphertext: %s
", base64.StdEncoding.EncodeToString(cipher))
}

// ParseRsaPublicKeyFromPem parse rsa public key from pem.
func ParseRsaPublicKeyFromPem(pubPEM []byte) (*rsa.PublicKey, error) {
	block, _ := pem.Decode(pubPEM)
	if block == nil {
		return nil, errors.New("failed to parse PEM block containing the key")
	}

	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	switch pub := pub.(type) {
	case *rsa.PublicKey:
		return pub, nil
	default:
	}
	return nil, errors.New("key type is not rsa")
}

// EncryptOAEP rsa encrypt oaep.
func EncryptOAEP(pubKey *rsa.PublicKey, message []byte) (ciphertext []byte, err error) {
	random := rand.Reader
	ciphertext, err = rsa.EncryptOAEP(sha256.New(), random, pubKey, message, nil)
	if err != nil {
		return nil, err
	}
	return
}

NOTE: You can also refer to the provided sample code in Python and Go for encrypting and encoding the Entity Secret.

d. Register the Entity Secret Ciphertext

After generating, encrypting, and encoding the Entity Secret, developers must register the Entity Secret ciphertext within the developer console.

  1. Access the Configurator Page in the developer console.
  2. Enter the Entity Secret Ciphertext generated in the previous step.
  3. Select "Register" to complete the Entity Secret Ciphertext registration.

Once registered, you are provided with a file to facilitate recovery in cases where the Entity Secret is lost. This file is used in subsequent Entity Secret reset procedures.

๐Ÿ“˜

Our platform does not store the Entity Secret, meaning only you can invoke private keys. However, it also means that you must keep the Entity Secret carefully yourself to ensure the security and accessibility of your Developer-Controlled wallets.

How to Re-Encrypt the Entity Secret

Circle's APIs Requiring Entity Secret Ciphertext enforce a unique Entity Secret Ciphertext for each API request. To create a unique Entity Secret Ciphertext token re-run the code from Step 1.c: Generate the Entity Secret Ciphertext. As long as the Entity Secret Ciphertext comes from the same registered entity secret, it will be valid for the API request.

Using the same Ciphertext for multiple requests will lead to rejection.

๐Ÿ“˜

One-time-use Entity Secret Ciphertext tokens ensure that even if an attacker captures the ciphertext from previous communication, they cannot exploit it in subsequent interactions.

2. Create a Wallet Set

Make a request to POST /developer/walletSets and create a wallet set providing a unique entity secret ciphertext as described in How to Re-Encrypt the Entity Secret.

curl --request POST \
     --url 'https://api.circle.com/v1/w3s/developer/walletSets' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --data '
{
  "idempotencyKey": "8f459a01-fa23-479d-8647-6fe05526c0df",
ย ย "name": "Entity WalletSet A",
ย ย "entitySecretCiphertext": "<ENTITY_SECRET_CIPHERTEXT>"
}
'
{
  "data": {
    "walletSet": {
      "id": "0189bc61-7fe4-70f3-8a1b-0d14426397cb",
      "custodyType": "DEVELOPER",
      "updateDate": "2023-08-03T17:10:51Z",
      "createDate": "2023-08-03T17:10:51Z"
    }
  }
}

3. Create a Wallet

Make a request to POST /developer/wallets using the walletSet.id from step 2 and a count of 1 as request parameters. NOTE: don't forget to generate a new entity secret ciphertext.

curl --request POST \
     --url 'https://api.circle.com/v1/w3s/developer/wallets' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --data '
{
  "idempotencyKey": "0189bc61-7fe4-70f3-8a1b-0d14426397cb",
  "blockchains": [
    "MATIC-MUMBAI"
  ],
  "count": 1,
  "entitySecretCiphertext": "<ENTITY_SECRET_CIPHERTEXT>",
  "walletSetId": "71f2a6b4-ffa7-417a-ad5b-fb928753edc8"
}
'
{
  "data": {
    "wallets": [
      {
        "id": "ce714f5b-0d8e-4062-9454-61aa1154869b",
        "state": "LIVE",
        "walletSetId": "0189bc61-7fe4-70f3-8a1b-0d14426397cb",
        "custodyType": "DEVELOPER",
        "address": "0xf5c83e5fede8456929d0f90e8c541dcac3d63835",
        "addressIndex": 0,
        "blockchain": "MATIC-MUMBAI",
        "accountType": "EOA",
        "updateDate": "2023-08-03T19:33:14Z",
        "createDate": "2023-08-03T19:33:14Z"
      }
    ]
  }
}

4. Acquire Gas Tokens

Obtain MATIC tokens from the Polygon Faucet and send them to the wallets wallets.address from the previous step's response body.ย 

5. Check the Wallet's Balance

Check the wallet balance by making a request to GET /wallets/{id}/balances ensuring the wallet successfully received the MATIC tokens.

curl --request GET \
     --url 'https://api.circle.com/v1/w3s/wallets/{id}/balances' \
     --header 'accept: application/json' \
     --header 'authorization: Bearer <API_KEY>'
{
  "data": {
    "tokenBalances": [
      {
        "token": {
          "id": "e4f549f9-a910-59b1-b5cd-8f972871f5db",
          "blockchain": "MATIC-MUMBAI",
          "name": "Polygon-Mumbai",
          "symbol": "MATIC-MUMBAI",
          "decimals": 18,
          "isNative": true,
          "updateDate": "2023-06-29T02:37:14Z",
          "createDate": "2023-06-29T02:37:14Z"
        },
        "amount": "0.2",
        "updateDate": "2023-08-03T22:22:07Z"
      }
    ]
  }
}

6. Transfer Tokens

Make a request to POST /developer/transactions/transfer with MATIC tokenId, Wallet 1's wallets.id, Wallet 2's wallets.address, as well as some predefined values for amount, and gas related request parameters.

curl --request POST \
     --url 'https://api.circle.com/v1/w3s/developer/transactions/transfer' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'authorization: Bearer <API_KEY>' \
     --data '
{
  "idempotencyKey": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "walletId": "ce714f5b-0d8e-4062-9454-61aa1154869b",
  "tokenId": "e4f549f9-a910-59b1-b5cd-8f972871f5db",
  "destinationAddress": "0xc90e058234d4b2db799d787a855ec68d801a53a3",
  "amounts": [
    ".01"
  ],
  "feeLevel": "MEDIUM",
  "entitySecretCiphertext": "<ENTITY_SECRET_CIPHERTEXT>",
}
'
{
  "data": {
    "id": "1af639ce-c8b2-54a6-af49-7aebc95aaac1",
    "state": "INITIATED"
  }
}

7. Check Transfer State

Make a request to GET /transactions/{id} to return available transactions.

curl --request GET \
     --url 'https://api.circle.com/v1/w3s/transactions/{id}' \
     --header 'accept: application/json' \
     --header 'authorization: Bearer <API_KEY>'
{
  "data": {
    "transaction": {
      "id": "1af639ce-c8b2-54a6-af49-7aebc95aaac1",
      "blockchain": "MATIC-MUMBAI",
      "tokenId": "e4f549f9-a910-59b1-b5cd-8f972871f5db",
      "walletId": "ce714f5b-0d8e-4062-9454-61aa1154869b",
      "sourceAddress": "0xf5c83e5fede8456929d0f90e8c541dcac3d63835",
      "destinationAddress": "0xc90e058234d4b2db799d787a855ec68d801a53a3",
      "transactionType": "OUTBOUND",
      "custodyType": "DEVELOPER",
      "state": "COMPLETE",
      "amounts": [
        "0.01"
      ],
      "nfts": null,
      "txHash": "0xc0337cad6164f22eeb37af3e21488f77e7bc5ba4a92a2293e0a8bb8beaa61c88",
      "blockHash": "0xbcaf5d0dc5d50250a137808ca81eb4abebf13a639398c44660b35e387e5ad2d3",
      "blockHeight": 38738377,
      "networkFee": "0.000035910000189",
      "firstConfirmDate": "2023-08-07T14:41:02Z",
      "operation": "TRANSFER",
      "feeLevel": "MEDIUM",
      "estimatedFee": {
        "gasLimit": "21000",
        "baseFee": "0.000000016",
        "priorityFee": "1.709999993",
        "maxFee": "1.710000025"
      },
      "refId": "",
      "abiParameters": null,
      "createDate": "2023-08-07T14:40:54Z",
      "updateDate": "2023-08-07T14:42:04Z"
    }
  }
}

Once the transaction.state is COMPLETE you can rest assured that the token has moved from wallet 1 to wallet 2.

You can also check the transaction using the txHash on Polygon Mumbai explorer and inspect the balance of each wallet using GET /wallets/{id}/balances.


Whatโ€™s Next

You have successfully created wallets and transferred a token across two wallets! If you are interested in learning more see the next guide