Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Address namespace #1729

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
});
Loading