Skip to content

Commit

Permalink
Merge pull request #5353 from BitGo/manas/WIN-4056-byron-support-for-ADA
Browse files Browse the repository at this point in the history


feat: Byron era support for ADA wallets
  • Loading branch information
manas-at-bitgo authored Jan 16, 2025
2 parents 596f6e0 + 3773ebf commit df7b341
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 30 deletions.
4 changes: 2 additions & 2 deletions modules/sdk-coin-ada/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class Transaction extends BaseTransaction {
for (let i = 0; i < this._transaction.body().outputs().len(); i++) {
const output = this._transaction.body().outputs().get(i);
result.outputs.push({
address: output.address().to_bech32(),
address: adaUtils.getAddressString(output.address()),
amount: output.amount().coin().to_str(),
multiAssets: output.amount().multiasset() || undefined,
});
Expand Down Expand Up @@ -237,7 +237,7 @@ export class Transaction extends BaseTransaction {
for (let i = 0; i < tx_outputs.len(); i++) {
const output = tx_outputs.get(i);
outputs.push({
address: output.address().to_bech32(),
address: adaUtils.getAddressString(output.address()),
value: output.amount().coin().to_str(),
});
}
Expand Down
17 changes: 7 additions & 10 deletions modules/sdk-coin-ada/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this._transactionOutputs.forEach((output) => {
const amount = CardanoWasm.BigNum.from_str(output.amount);
outputs.add(
CardanoWasm.TransactionOutput.new(
CardanoWasm.Address.from_bech32(output.address),
CardanoWasm.Value.new(amount)
)
CardanoWasm.TransactionOutput.new(util.getWalletAddress(output.address), CardanoWasm.Value.new(amount))
);
totalAmountToSend = totalAmountToSend.checked_add(amount);
});
Expand All @@ -160,7 +157,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
// estimate fee
// add extra output for the change
if (this._changeAddress && this._senderBalance) {
const changeAddress = CardanoWasm.Address.from_bech32(this._changeAddress);
const changeAddress = util.getWalletAddress(this._changeAddress);
const utxoBalance = CardanoWasm.BigNum.from_str(this._senderBalance);

const adjustment = BigNum.from_str('2000000');
Expand Down Expand Up @@ -188,7 +185,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this._multiAssets.forEach((asset) => {
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
// changeAddress is the root address, which is where we want the tokens assets to be sent to
const toAddress = CardanoWasm.Address.from_bech32(this._changeAddress);
const toAddress = util.getWalletAddress(this._changeAddress);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const assetName = CardanoWasm.AssetName.new(Buffer.from(asset.asset_name, 'hex'));
Expand Down Expand Up @@ -301,7 +298,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
const quantity = assets!.get(assetName);
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
const outputAmount = CardanoWasm.BigNum.from_str(output.amount);
const toAddress = CardanoWasm.Address.from_bech32(output.address);
const toAddress = util.getWalletAddress(output.address);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const multiAsset = CardanoWasm.MultiAsset.new();
Expand All @@ -314,14 +311,14 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
} else {
outputs.add(
CardanoWasm.TransactionOutput.new(
CardanoWasm.Address.from_bech32(output.address),
util.getWalletAddress(output.address),
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(output.amount))
)
);
}
});
if (this._changeAddress && this._senderBalance) {
const changeAddress = CardanoWasm.Address.from_bech32(this._changeAddress);
const changeAddress = util.getWalletAddress(this._changeAddress);
const utxoBalance = CardanoWasm.BigNum.from_str(this._senderBalance);

const adjustment = BigNum.from_str('2000000');
Expand All @@ -348,7 +345,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this._multiAssets.forEach((asset) => {
let txOutputBuilder = CardanoWasm.TransactionOutputBuilder.new();
// changeAddress is the root address, which is where we want the tokens assets to be sent to
const toAddress = CardanoWasm.Address.from_bech32(this._changeAddress);
const toAddress = util.getWalletAddress(this._changeAddress);
txOutputBuilder = txOutputBuilder.with_address(toAddress);
let txOutputAmountBuilder = txOutputBuilder.next();
const assetName = CardanoWasm.AssetName.new(Buffer.from(asset.asset_name, 'hex'));
Expand Down
102 changes: 87 additions & 15 deletions modules/sdk-coin-ada/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddressFormat, BaseUtils } from '@bitgo/sdk-core';
import { AddressFormat, BaseUtils, InvalidAddressError } from '@bitgo/sdk-core';
import {
BaseAddress,
PublicKey,
Expand All @@ -11,6 +11,10 @@ import {
Ed25519KeyHash,
ScriptHash,
DRepKind,
ByronAddress,
Address,
EnterpriseAddress,
PointerAddress,
} from '@emurgo/cardano-serialization-lib-nodejs';
import { KeyPair } from './keyPair';
import { bech32 } from 'bech32';
Expand Down Expand Up @@ -149,26 +153,38 @@ export class Utils implements BaseUtils {
const POINTER_ADDR_LEN = 52;
const VALIDATOR_ADDR_LEN = 56;

// test if this is a bech32 address first
if (new RegExp(bech32PrefixList.join('|')).test(address)) {
//Check for Shelley-era (Bech32) addresses
if (new RegExp(`^(${bech32PrefixList.join('|')})`).test(address)) {
try {
const decodedBech = bech32.decode(address, 108);
const wordLength = decodedBech.words.length;
if (!bech32PrefixList.includes(decodedBech.prefix)) {
return false;
if (
bech32PrefixList.includes(decodedBech.prefix) &&
(wordLength === BASE_ADDR_LEN ||
wordLength === REWARD_AND_ENTERPRISE_ADDR_LEN ||
wordLength === POINTER_ADDR_LEN)
) {
return true;
}
return (
wordLength === BASE_ADDR_LEN ||
wordLength === REWARD_AND_ENTERPRISE_ADDR_LEN ||
wordLength === POINTER_ADDR_LEN
);
} catch (err) {
return false;
} catch (e) {
console.log(`Address: ${address} failed Bech32 test with error: ${e}`);
}
} else {
// maybe this is a validator address
return new RegExp(`^(?!pool)[a-z0-9]\{${VALIDATOR_ADDR_LEN}\}$`).test(address);
}

//Check for Validator addresses
if (new RegExp(`^(?!pool)[a-z0-9]{${VALIDATOR_ADDR_LEN}}$`).test(address)) {
return true;
}

//Check for Byron-era address
try {
return ByronAddress.is_valid(address);
} catch (e) {
console.log(`Address: ${address} failed Byron test with error: ${e}`);
console.log(e.stack);
}

return false;
}

/** @inheritdoc */
Expand Down Expand Up @@ -228,6 +244,62 @@ export class Utils implements BaseUtils {
: Buffer.from(serializedTx, 'base64');
return Buffer.from(CardanoTransaction.from_bytes(bufferRawTransaction).body().to_bytes()).toString('hex');
}

/**
* Decode wallet address from string.
* Attempts to decode as Shelley (bech32) first, then Byron (base58).
* @param {string} address - Valid Byron or Shelley-era address.
* @returns {Address} - Valid address object.
* @throws {InvalidAddressError} If the address is neither valid Shelley nor Byron.
*/
getWalletAddress(address: string): Address {
if (!address || typeof address !== 'string') {
throw new InvalidAddressError('Provided address is not a valid string');
}

// Try decoding as a Shelley (bech32) address first
try {
return Address.from_bech32(address);
} catch (e) {
console.error(`Could not decode shelly address from string '${address}'`);
}

// Try decoding as a Byron (base58) address later
try {
return ByronAddress.from_base58(address).to_address();
} catch (e) {
console.error(`Could not decode byron address from string '${address}'`);
}
throw new InvalidAddressError('Provided string is not a valid Shelley or Byron address');
}

/**
* Decode address string from Address object.
* Attempts to decode as Shelley (bech32) first, then Byron (base58).
* @param {Address} address - Valid Address object
* @returns {string} - Valid Byron or Shelley-era address string.
* @throws {InvalidAddressError} If the Address object is neither valid Shelley nor Byron.
*/
getAddressString(address: Address): string {
// Check all Shelley address types
if (
BaseAddress.from_address(address) ||
EnterpriseAddress.from_address(address) ||
RewardAddress.from_address(address) ||
PointerAddress.from_address(address)
) {
return address.to_bech32();
}

// If not Shelley, try Byron
const byronAddress = ByronAddress.from_address(address);
if (byronAddress) {
return byronAddress.to_base58();
}

// If neither, it's invalid
throw new InvalidAddressError('Provided Address is not a valid Shelley or Byron address');
}
}

const utils = new Utils();
Expand Down
Loading

0 comments on commit df7b341

Please sign in to comment.