Skip to content

Commit

Permalink
feat: Add Address namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Sep 17, 2024
1 parent 2613915 commit 1588477
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/transactions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export * from './utils';
export * from './address';
export * from './wire';

export * from './namespaces';

/**
* ### `Cl.` Clarity Value Namespace
* The `Cl` namespace is provided as a convenience to build/parse Clarity Value objects.
Expand Down
17 changes: 17 additions & 0 deletions packages/transactions/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,23 @@ export function signMessageHashRsv({
return { ...messageSignature, data: signatureVrsToRsv(messageSignature.data) };
}

/**
* Convert a private key to a single-sig address.
* @returns A Stacks address string (encoded with c32check)
* @example
* ```
* const address = privateKeyToAddress("73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801");
* // SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR
* ```
*/
export function privateKeyToAddress(
privateKey: PrivateKey,
network?: StacksNetworkName | StacksNetwork
): string {
const publicKey = privateKeyToPublic(privateKey);
return publicKeyToAddressSingleSig(publicKey, network);
}

/**
* Convert a public key to an address.
* @returns A Stacks address string (encoded with c32check)
Expand Down
108 changes: 108 additions & 0 deletions packages/transactions/src/namespaces/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { c32address, c32addressDecode } from 'c32check';
import { AddressVersion } from '../constants';
import { privateKeyToAddress, publicKeyToAddressSingleSig } from '../keys';
import { AddressString, ContractIdString } from '../types';

const C32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

export type AddressRepr = { hash160: string; contractName?: string } & (
| {
version: AddressVersion;
versionChar: string;
}
| {
version: AddressVersion;
}
| {
versionChar: string;
}
);

/**
* Parse a C32 Stacks address string to an address object.
* @param address - The address string to parse.
* @example
* ```ts
* import { Address } from '@stacks/transactions';
*
* const address = Address.parse('SP000000000000000000002Q6VF78');
* // { version: 22, versionChar: 'P', hash160: '0000000000000000000000000000000000000000' }
*
* const address = Address.parse('ST000000000000000000002AMW42H.pox');
* // { version: 22, versionChar: 'P', hash160: '0000000000000000000000000000000000000000', contractName: 'pox' }
* ```
*/
export function parse(
address:
| AddressString
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
| ContractIdString
): AddressRepr {
const [addr, contractName] = address.split('.');
const parsed = c32addressDecode(addr);
return {
version: parsed[0],
versionChar: C32[parsed[0]],
hash160: parsed[1],
contractName: contractName,
};
}

/**
* Stringify an address to the C32 address format.
* @param address - The address object to stringify.
* @example
* ```ts
* import { Address } from '@stacks/transactions';
*
* const address = Address.stringify({ version: 22, hash160: '0000000000000000000000000000000000000000' });
* console.log(address); // 'SP000000000000000000002Q6VF78'
*
* const address = Address.stringify({ versionChar: 'P', hash160: '0000000000000000000000000000000000000000' });
* console.log(address); // 'SP000000000000000000002Q6VF78'
* ```
*/
export function stringify(address: AddressRepr): string {
const version =
'version' in address ? address.version : C32.indexOf(address.versionChar.toUpperCase());
const addr = c32address(version, address.hash160);

if (address.contractName) return `${addr}.${address.contractName}`;
return addr;
}

/**
* Convert a private key to a single-sig C32 Stacks address.
* @param privateKey - The private key to convert.
* @returns The address string.
*
* @example
* ```ts
* import { Address } from '@stacks/transactions';
*
* const address = Address.fromPrivateKey('73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801');
* // 'SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR'
*
* const address = Address.fromPrivateKey('73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801', 'testnet');
* // 'ST10J81WVGVB3M4PHQN4Q4G0R8586TBJH94CGRESQ'
* ```
*/
export const fromPrivateKey = privateKeyToAddress;

/**
* Convert a public key to a single-sig C32 Stacks address.
* @param publicKey - The public key to convert.
* @returns The address string.
*
* @example
* ```ts
* import { Address } from '@stacks/transactions';
*
* const address = Address.fromPublicKey('0316e35d38b52d4886e40065e4952a49535ce914e02294be58e252d1998f129b19');
* // 'SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR'
*
* const address = Address.fromPublicKey('0316e35d38b52d4886e40065e4952a49535ce914e02294be58e252d1998f129b19', 'testnet');
* // 'ST10J81WVGVB3M4PHQN4Q4G0R8586TBJH94CGRESQ'
* ```
*/
export const fromPublicKey = publicKeyToAddressSingleSig;
1 change: 1 addition & 0 deletions packages/transactions/src/namespaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Address from './address';
15 changes: 14 additions & 1 deletion packages/transactions/tests/keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
signatureRsvToVrs,
utf8ToBytes,
} from '@stacks/common';
import { AddressVersion, TransactionVersion } from '@stacks/network';
import { AddressVersion, STACKS_TESTNET, TransactionVersion } from '@stacks/network';
import { ec as EC } from 'elliptic';
import {
PubKeyEncoding,
Expand All @@ -24,6 +24,7 @@ import {
getAddressFromPrivateKey,
getAddressFromPublicKey,
makeRandomPrivKey,
privateKeyToAddress,
privateKeyToHex,
privateKeyToPublic,
publicKeyFromSignatureRsv,
Expand Down Expand Up @@ -330,3 +331,15 @@ describe(publicKeyToAddress.name, () => {
expect(address).toBe('SPAW66WC3G8WA5F28JVNG1NTRJ6H76E7EN5H6QQD');
});
});

describe(privateKeyToAddress.name, () => {
it('should return the correct single-sig address', () => {
const privateKey = '73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801';

const address = privateKeyToAddress(privateKey);
expect(address).toBe('SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR');

const addressTestnet = privateKeyToAddress(privateKey, STACKS_TESTNET);
expect(addressTestnet).toBe('ST10J81WVGVB3M4PHQN4Q4G0R8586TBJH94CGRESQ');
});
});
85 changes: 85 additions & 0 deletions packages/transactions/tests/namespaces/address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Address } from '../../src';

describe('Address Namespace', () => {
describe(Address.parse, () => {
it('should parse addresses correctly', () => {
const address = Address.parse('SP000000000000000000002Q6VF78');
expect(address).toEqual({ version: 22, versionChar: 'P', hash160: '0'.repeat(40) });

const addressWithVersionChar = Address.parse('SP000000000000000000002Q6VF78');
expect(addressWithVersionChar).toEqual({
version: 22,
versionChar: 'P',
hash160: '0'.repeat(40),
});

const addressWithContractId = Address.parse('ST000000000000000000002AMW42H.pox');
expect(addressWithContractId).toEqual({
version: 26,
versionChar: 'T',
hash160: '0'.repeat(40),
contractName: 'pox',
});
});

it('should throw an error for invalid addresses', () => {
expect(() => Address.parse('invalid')).toThrow('Invalid c32 address: must start with "S"');
});

it('should throw an error for checksum mismatches', () => {
expect(() => Address.parse('ST000BLABLA')).toThrow(
'Invalid c32check string: checksum mismatch'
);
});
});

describe(Address.stringify, () => {
it('should return the correct address', () => {
const addressWithVersion = Address.stringify({ version: 22, hash160: '0'.repeat(40) });
expect(addressWithVersion).toBe('SP000000000000000000002Q6VF78');

const addressWithVersionChar = Address.stringify({
versionChar: 'P',
hash160: '0'.repeat(40),
});
expect(addressWithVersionChar).toBe('SP000000000000000000002Q6VF78');

const addressWithContractId = Address.stringify({
versionChar: 'T',
hash160: '0'.repeat(40),
contractName: 'pox',
});
expect(addressWithContractId).toBe('ST000000000000000000002AMW42H.pox');
});
});

describe(Address.fromPrivateKey, () => {
it('should return the correct address', () => {
const address = Address.fromPrivateKey(
'73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801'
);
expect(address).toBe('SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR');

const addressTestnet = Address.fromPrivateKey(
'73a2f291df5a8ce3ceb668a25ac7af45639513af7596d710ddf59f64f484fd2801',
'testnet'
);
expect(addressTestnet).toBe('ST10J81WVGVB3M4PHQN4Q4G0R8586TBJH94CGRESQ');
});
});

describe(Address.fromPublicKey, () => {
it('should return the correct address', () => {
const address = Address.fromPublicKey(
'0316e35d38b52d4886e40065e4952a49535ce914e02294be58e252d1998f129b19'
);
expect(address).toBe('SP10J81WVGVB3M4PHQN4Q4G0R8586TBJH948RESDR');

const addressTestnet = Address.fromPublicKey(
'0316e35d38b52d4886e40065e4952a49535ce914e02294be58e252d1998f129b19',
'testnet'
);
expect(addressTestnet).toBe('ST10J81WVGVB3M4PHQN4Q4G0R8586TBJH94CGRESQ');
});
});
});
25 changes: 25 additions & 0 deletions packages/transactions/tests/namespaces/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Address } from '../../src';

describe('Address', () => {
describe(Address.parse, () => {});

describe(Address.stringify, () => {
it('should return the correct address', () => {
const addressWithVersion = Address.stringify({ version: 22, hash160: '0'.repeat(40) });
expect(addressWithVersion).toBe('SP000000000000000000002Q6VF78');

const addressWithVersionChar = Address.stringify({
versionChar: 'P',
hash160: '0'.repeat(40),
});
expect(addressWithVersionChar).toBe('SP000000000000000000002Q6VF78');

const addressWithContractId = Address.stringify({
versionChar: 'T',
hash160: '0'.repeat(40),
contractName: 'pox',
});
expect(addressWithContractId).toBe('ST000000000000000000002Q6VF78.pox');
});
});
});

0 comments on commit 1588477

Please sign in to comment.