Crypto-as-a-Service: Transfer Between Go Accounts
Build, authenticate, and send an off-chain book transfer between two Go Accounts. See the Guide.
-
Build a transaction from the source Go Account to the destination Go Account. Pass the destination Go Account wallet ID as the value of the
addressfield in the recipient object — the recipient object usesaddress, notwalletId. The response payload containsshortCircuitBlockchainTransfer: trueandisIntraJXTransfer: true, confirming that BitGo recognized this as an off-chain book transfer. -
Use your Go Account passphrase to authenticate the transaction. To ensure your passphrase isn't passed over the Internet, you must use BitGo Express in external-signing mode, the JavaScript SDK, or Java. If you're using Express in external-signing mode, configure your Go Account passphrase as the
WALLET_<walletId>_PASSPHRASEenvironment variable. -
Send the signed payload to BitGo. The
halfSignedobject takes only the two values returned by Step 2:payload(the transaction payload string from Step 1, returned verbatim by Step 2) andsignature(the hex-encoded signature from Step 2). All transaction details are already encoded inside thepayloadstring. ThetransactionTypefield in the response confirms that BitGo processed this as aBOOK_TRANSFER. -
If you configure an approval requirement for transfers, the transaction remains pending until another admin approves it — you can't approve your own transactions. Once approved, BitGo processes the book transfer and updates the ledger balances for both Go Accounts.
// 1. Build Transaction
export COIN="<CRYPTO_ASSET_ID>" # Crypto asset IDs in Go Accounts always start with "ofc" (such as ofctbtc)
export WALLET_ID="<SOURCE_GO_ACCOUNT_WALLET_ID>"
export SERVICE_USER_TOKEN="<SERVICE_USER_ACCESS_TOKEN>"
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"
export DESTINATION_WALLET_ID="<DESTINATION_GO_ACCOUNT_WALLET_ID>"
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/build \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"recipients": [
{
"amount": "'"$AMOUNT"'",
"address": "'"$DESTINATION_WALLET_ID"'"
}
]
}'
// 2. Authenticate Transaction
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export WALLET_PASSPHRASE="<YOUR_GO_ACCOUNT_PASSPHRASE>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/ofc/signPayload \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"walletId": "'"$WALLET_ID"'",
"walletPassphrase": "'"$WALLET_PASSPHRASE"'",
"payload": "<PAYLOAD_FROM_BUILD_STEP>"
}'
// 3. Send Transaction
export PAYLOAD="<PAYLOAD_FROM_STEP_2>"
export SIGNATURE="<SIGNATURE_FROM_STEP_2>"
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/send \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"halfSigned": {
"payload": "'"$PAYLOAD"'",
"signature": "'"$SIGNATURE"'"
}
}'
// 4. Approve Transaction (Optional)
export APPROVAL_ID="<APPROVAL_ID>"
export OTP="<OTP>"
curl -X PUT \
https://app.bitgo-test.com/api/v2/pendingApprovals/$APPROVAL_ID \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"state": "approved",
"otp": "'"$OTP"'"
}'
// 1. Build Transaction
let params = {
recipients: [
{
amount: 1000,
address: '654ec786c07fe6dc0dcfe03916ec4bb0',
},
],
};
wallet.prebuildTransaction(params).then(function (transaction) {
console.dir(transaction);
});
// 2. Authenticate Transaction
const wallet = await bitgo.coin('ofc').wallets().get({ id: goAccountId });
if (wallet === undefined) {
throw new Error(`Could not find OFC wallet ${goAccountId}`);
}
const walletPassphrase = "<YOUR_WALLET_PASSPHRASE>";
const tradingAccount = wallet.toTradingAccount();
const stringifiedPayload = JSON.stringify(<payload_returned_by_tx/build_api>);
const signature = await tradingAccount.signPayload({
payload: stringifiedPayload,
walletPassphrase,
});
// 3. Send Transaction
// stringifiedPayload and signature come from Steps 1 and 2
const sendUrl = wallet.baseCoin.url('/wallet/' + wallet.id() + '/tx/send');
await bitgo
.post(sendUrl)
.send({ halfSigned: { payload: stringifiedPayload, signature } })
.result();
// 4. Approve Transaction (Optional)
const baseCoin = bitgo.coin(initialPendingApproval.coin);
const pendingApproval = await baseCoin.pendingApprovals().get({ id: initialPendingApproval.id });
const result = await pendingApproval.approve(params);
// 1. Build Transaction
// Use the cURL tab for this step
// 2. Authenticate Transaction
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.security.spec.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.google.gson.*;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.crypto.DeterministicKey;
public class SignPayload {
public static void main(String[] args) throws Exception {
System.out.println("=== Sign Payload for Go Account Withdrawal ===\n");
// Configuration - replace with your values
String walletPassphrase = "your_wallet_passphrase";
String encryptedPrivateKey = "{\"iv\":...";
String payloadToSign = "{\"coin\":...";
// Step 1: Decrypt private key
System.out.println("Step 1: Decrypt Private Key");
System.out.println("-".repeat(50));
Gson gson = new Gson();
JsonObject data = gson.fromJson(encryptedPrivateKey, JsonObject.class);
byte[] salt = Base64.getDecoder().decode(data.get("salt").getAsString());
byte[] iv = Base64.getDecoder().decode(data.get("iv").getAsString());
byte[] ciphertextWithTag = Base64.getDecoder().decode(data.get("ct").getAsString());
int iterations = data.get("iter").getAsInt();
int keySize = data.get("ks").getAsInt();
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(walletPassphrase.toCharArray(), salt, iterations, keySize);
SecretKey tmp = factory.generateSecret(spec);
SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
byte[] decrypted = cipher.doFinal(ciphertextWithTag);
String decryptedPrv = new String(decrypted, StandardCharsets.UTF_8);
System.out.println("✓ Private key decrypted");
System.out.println();
// Step 2: Sign payload
System.out.println("Step 2: Sign Payload");
System.out.println("-".repeat(50));
DeterministicKey key = DeterministicKey.deserializeB58(decryptedPrv, org.bitcoinj.params.MainNetParams.get());
ECKey ecKey = ECKey.fromPrivate(key.getPrivKeyBytes());
// Bitcoin message format
String messagePrefix = "\u0018Bitcoin Signed Message:\n";
byte[] payloadBytes = payloadToSign.getBytes(StandardCharsets.UTF_8);
byte[] prefixBytes = messagePrefix.getBytes(StandardCharsets.UTF_8);
byte[] lengthVarint = encodeVarint(payloadBytes.length);
byte[] messageToSign = new byte[prefixBytes.length + lengthVarint.length + payloadBytes.length];
System.arraycopy(prefixBytes, 0, messageToSign, 0, prefixBytes.length);
System.arraycopy(lengthVarint, 0, messageToSign, prefixBytes.length, lengthVarint.length);
System.arraycopy(payloadBytes, 0, messageToSign, prefixBytes.length + lengthVarint.length, payloadBytes.length);
// Double SHA-256 hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] firstHash = digest.digest(messageToSign);
byte[] messageHash = digest.digest(firstHash);
// Sign
ECKey.ECDSASignature signature = ecKey.sign(Sha256Hash.wrap(messageHash));
// Create 65-byte recoverable signature
byte[] result = new byte[65];
// Calculate recovery ID
int recoveryId = -1;
for (int i = 0; i < 4; i++) {
try {
ECKey recovered = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(messageHash), true);
if (recovered != null && Arrays.equals(recovered.getPubKey(), ecKey.getPubKey())) {
recoveryId = i;
break;
}
} catch (Exception e) {
// Continue trying
}
}
if (recoveryId == -1) {
throw new RuntimeException("Could not determine signature recovery ID");
}
result[0] = (byte) (31 + recoveryId);
copyBigIntegerTo32Bytes(signature.r.toByteArray(), result, 1);
copyBigIntegerTo32Bytes(signature.s.toByteArray(), result, 33);
String hexSignature = bytesToHex(result);
System.out.println("✓ Payload signed");
System.out.println();
System.out.println("Signature (hex):");
System.out.println(hexSignature);
System.out.println();
System.out.println("✓ Use this signature with the /tx/send endpoint");
}
private static byte[] encodeVarint(long value) {
if (value < 253) {
return new byte[] { (byte) value };
} else if (value <= 0xFFFF) {
byte[] result = new byte[3];
result[0] = (byte) 0xFD;
result[1] = (byte) (value & 0xFF);
result[2] = (byte) ((value >> 8) & 0xFF);
return result;
}
throw new RuntimeException("Payload too large");
}
private static void copyBigIntegerTo32Bytes(byte[] source, byte[] dest, int destOffset) {
int sourceOffset = 0;
int sourceLength = source.length;
if (sourceLength > 32) {
sourceOffset = 1;
sourceLength = 32;
}
int destStart = destOffset + (32 - sourceLength);
System.arraycopy(source, sourceOffset, dest, destStart, sourceLength);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
// 3. Send Transaction
// Use the cURL tab for this step
// 4. Approve Transaction (Optional)
// Use the cURL tab for this step
// 1. Build Transaction Response
{
"payload": "{\"coin\":\"ofctbtc\",\"recipients\":[{\"address\":\"2NDBpypweakRZzgbWqxEK42pnxgfYzd5iuY\",\"amount\":\"10000\"}],\"fromAccount\":\"63ff43c474598902272894f328452ad7\",\"nonce\":\"80bc93b5-eeec-4c11-afca-f6dae4bba2f2\",\"timestamp\":\"2023-09-28T10:55:39.135Z\",\"feeString\":\"0\",\"shortCircuitBlockchainTransfer\":true,\"isIntraJXTransfer\":true}",
"feeInfo": { "feeString": "0" },
"coin": "ofc",
"token": "ofctbtc"
}
// 2. Authenticate Transaction Response
{
"payload": "{\"coin\":\"ofctbtc\",\"recipients\":[{\"address\":\"2NDBpypweakRZzgbWqxEK42pnxgfYzd5iuY\",\"amount\":\"10000\"}],\"fromAccount\":\"63ff43c474598902272894f328452ad7\",\"nonce\":\"80bc93b5-eeec-4c11-afca-f6dae4bba2f2\",\"timestamp\":\"2023-09-28T10:55:39.135Z\",\"feeString\":\"0\",\"shortCircuitBlockchainTransfer\":true,\"isIntraJXTransfer\":true}",
"signature": "1f989926e3fc8ed0138bd0e188e90e2c5321ef86734896886c004005740a005dfd2008142dbc48aee68d1f576c4a9ac558e5b7b4acbd5aa765710ea636aca9d032"
}
// 3. Send Transaction Response
{
"transfer": {
"id": "65155c4a72fddb000774edbee5fa75fd",
"coin": "ofctbtc",
"wallet": "63ff43c474598902272894f328452ad7",
"walletType": "trading",
"enterprise": "63f31592e277e50837712300495902c7",
"txid": "ddd6875812fa59f899a68059ff303bfa6d39fc350ee687c407383d0444b43d2b",
"height": 999999999,
"heightId": "999999999-65155c4a72fddb000774edbe",
"date": "2023-09-28T10:58:18.272Z",
"type": "send",
"value": -10000,
"valueString": "-10000",
"baseValue": -10000,
"baseValueString": "-10000",
"feeString": "0",
"payGoFee": 0,
"payGoFeeString": "0",
"state": "signed",
"instant": false,
"isReward": false,
"isFee": false,
"tags": [
"63ff43c474598902272894f328452ad7",
"63f31592e277e50837712300495902c7"
],
"entries": [
{
"address": "63ff43c474598902272894f328452ad7",
"wallet": "63ff43c474598902272894f328452ad7",
"value": -10000,
"valueString": "-10000"
},
{
"address": "2NDBpypweakRZzgbWqxEK42pnxgfYzd5iuY",
"value": 10000,
"valueString": "10000"
}
]
},
"txid": "ddd6875812fa59f899a68059ff303bfa6d39fc350ee687c407383d0444b43d2b",
"tx": {
"transaction": {
"coin": "ofctbtc",
"recipients": [
{
"address": "2NDBpypweakRZzgbWqxEK42pnxgfYzd5iuY",
"amount": "10000"
}
],
"fromAccount": "63ff43c474598902272894f328452ad7",
"nonce": "80bc93b5-eeec-4c11-afca-f6dae4bba2f2",
"timestamp": "2023-09-28T10:55:39.135Z",
"feeString": "0",
"shortCircuitBlockchainTransfer": true,
"isIntraJXTransfer": true
},
"signature": "1f989926e3fc8ed0138bd0e188e90e2c5321ef86734896886c004005740a005dfd2008142dbc48aee68d1f576c4a9ac558e5b7b4acbd5aa765710ea636aca9d032",
"transactionType": "BOOK_TRANSFER"
},
"status": "signed"
}
// 4. Approve Transaction (Optional) Response
{
"id": "65155c4a72fddb000774edbee5fa75fd",
"coin": "ofctbtc",
"wallet": "63ff43c474598902272894f328452ad7",
"enterprise": "63f31592e277e50837712300495902c7",
"state": "approved"
}