Skip to content

Commit

Permalink
feat: update connect to be stacks.js v7 compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Oct 18, 2024
1 parent 6806082 commit 4374364
Show file tree
Hide file tree
Showing 16 changed files with 13,424 additions and 18,995 deletions.
32,194 changes: 13,271 additions & 18,923 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions packages/connect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,11 @@ Here, we are sending `10000` micro-STX tokens to a recipient address.

```js
import { openSTXTransfer } from '@stacks/connect';
import { StacksTestnet } from '@stacks/network';
import { AnchorMode, PostConditionMode } from '@stacks/transactions';
import { userSession } from './userSession';

openSTXTransfer({
network: new StacksTestnet(), // which network to use; use `new StacksMainnet()` for mainnet
network: 'testnet', // which network to use; ('mainnet' or 'testnet')
anchorMode: AnchorMode.Any, // which type of block the tx should be mined in

recipient: 'ST39MJ145BR6S8C315AG2BD61SJ16E208P1FDK3AK', // which address we are sending to
Expand All @@ -105,14 +104,13 @@ Here, we are passing our pick `Alice` to an imaginary deployed voting smart-cont

```js
import { openContractCall } from '@stacks/connect';
import { StacksTestnet } from '@stacks/network';
import { AnchorMode, PostConditionMode, stringUtf8CV } from '@stacks/transactions';
import { userSession } from './userSession';

const pick = stringUtf8CV('Alice');

openContractCall({
network: new StacksTestnet(),
network: 'testnet', // which network to use; ('mainnet' or 'testnet')
anchorMode: AnchorMode.Any, // which type of block the tx should be mined in

contractAddress: 'ST39MJ145BR6S8C315AG2BD61SJ16E208P1FDK3AK',
Expand Down Expand Up @@ -217,7 +215,7 @@ A glossary of the most common options of `openSTXTransfer` and `openContractCall

| | Default | Description | Type | Example |
| :------------------ | :------------------ | :-------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | :------------------------ |
| `network` | Mainnet | The network to broadcast the transaction to | [StacksNetwork](https://stacks.js.org/classes/network.StacksNetwork.html) | `new StacksMainnet()` |
| `network` | Mainnet | The network to broadcast the transaction to | string | 'mainnet' |
| `anchorMode` | Any | The type of block the transaction should be mined in | [AnchorMode Enum](https://stacks.js.org/enums/transactions.AnchorMode.html) | `AnchorMode.OnChainOnly` |
| `memo` | _Empty_ `''` | The memo field (used for additional data) | `string` | `'a memo'` |
| `fee` | _Handled by Wallet_ | The transaction fee (the wallet will estimate fees as well) | Integer (e.g. `number`, `bigint`) | `1000` |
Expand Down
11 changes: 7 additions & 4 deletions packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@stacks/auth": "^6.1.1",
"@stacks/auth": "^7.0.0-next.70",
"@stacks/common": "^7.0.0-next.70",
"@stacks/connect-ui": "6.4.1",
"@stacks/network": "^6.1.1",
"@stacks/profile": "^6.1.1",
"@stacks/transactions": "^6.1.1",
"@stacks/network": "^7.0.0-next.70",
"@stacks/network-v6": "npm:@stacks/network@^6.16.0",
"@stacks/profile": "^7.0.0-next.70",
"@stacks/transactions": "^7.0.0-next.70",
"@stacks/transactions-v6": "npm:@stacks/transactions@^6.16.0",
"jsontokens": "^4.0.1"
},
"sideEffects": false,
Expand Down
8 changes: 3 additions & 5 deletions packages/connect/src/bitcoin/psbt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { StacksTestnet } from '@stacks/network';
import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens';

import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions';
import { PsbtPayload, PsbtPopup, PsbtRequestOptions } from '../types/bitcoin';
import { getStacksProvider } from '../utils';
import { StacksProvider } from '../types';
import { PsbtPayload, PsbtPopup, PsbtRequestOptions } from '../types/bitcoin';
import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils';

// eslint-disable-next-line @typescript-eslint/require-await
async function signPayload(payload: PsbtPayload, privateKey: string) {
Expand All @@ -14,7 +12,7 @@ async function signPayload(payload: PsbtPayload, privateKey: string) {
}

export function getDefaultPsbtRequestOptions(options: PsbtRequestOptions) {
const network = options.network || new StacksTestnet();
const network = legacyNetworkFromConnectNetwork(options.network);
const userSession = getUserSession(options.userSession);
const defaults: PsbtRequestOptions = {
...options,
Expand Down
5 changes: 2 additions & 3 deletions packages/connect/src/profile/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { StacksTestnet } from '@stacks/network';
import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens';
import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions';
import {
Expand All @@ -8,7 +7,7 @@ import {
StacksProvider,
} from '../types';

import { getStacksProvider } from '../utils';
import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils';

// eslint-disable-next-line @typescript-eslint/require-await
async function signPayload(payload: ProfileUpdatePayload, privateKey: string) {
Expand All @@ -18,7 +17,7 @@ async function signPayload(payload: ProfileUpdatePayload, privateKey: string) {
}

export function getDefaultProfileUpdateRequestOptions(options: ProfileUpdateRequestOptions) {
const network = options.network || new StacksTestnet();
const network = legacyNetworkFromConnectNetwork(options.network);
const userSession = getUserSession(options.userSession);
const defaults: ProfileUpdateRequestOptions = {
...options,
Expand Down
18 changes: 9 additions & 9 deletions packages/connect/src/signature/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { StacksTestnet } from '@stacks/network';
import { ChainID } from '@stacks/transactions';
import { ChainId } from '@stacks/network';
import { createUnsecuredToken, TokenSigner } from 'jsontokens';
import { getKeys, getUserSession, hasAppPrivateKey } from '../transactions';
import { StacksProvider } from '../types';
import {
CommonSignatureRequestOptions,
SignatureOptions,
SignaturePayload,
SignaturePopup,
SignatureRequestOptions,
} from '../types/signature';
import { getStacksProvider } from '../utils';
import { StacksProvider } from '../types';
import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils';

function getStxAddress(options: CommonSignatureRequestOptions) {
const { userSession, network } = options;
const { userSession, network: _network } = options;

if (!userSession || !network) return undefined;
if (!userSession || !_network) return undefined;
const stxAddresses = userSession?.loadUserData().profile?.stxAddress;
const chainIdToKey = {
[ChainID.Mainnet]: 'mainnet',
[ChainID.Testnet]: 'testnet',
[ChainId.Mainnet]: 'mainnet',
[ChainId.Testnet]: 'testnet',
};
const network = legacyNetworkFromConnectNetwork(_network);
const address: string | undefined = stxAddresses?.[chainIdToKey[network.chainId]];
return address;
}
Expand All @@ -33,7 +33,7 @@ async function signPayload(payload: SignaturePayload, privateKey: string) {
}

export function getDefaultSignatureRequestOptions(options: CommonSignatureRequestOptions) {
const network = options.network || new StacksTestnet();
const network = legacyNetworkFromConnectNetwork(options.network);
const userSession = getUserSession(options.userSession);
const defaults: CommonSignatureRequestOptions = {
...options,
Expand Down
33 changes: 25 additions & 8 deletions packages/connect/src/signature/structuredData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { bytesToHex } from '@stacks/common';
import {
serializeCV as legacySerializeCV,
ClarityValue as LegacyClarityValue,
TupleCV as LegacyTupleCV,
} from '@stacks/transactions-v6';
import { serializeCV } from '@stacks/transactions';
import { createUnsecuredToken, TokenSigner } from 'jsontokens';
import { createUnsecuredToken, Json, TokenSigner } from 'jsontokens';
import { getDefaultSignatureRequestOptions } from '.';
import { getKeys, hasAppPrivateKey } from '../transactions';
import {
Expand All @@ -24,12 +29,24 @@ async function generateTokenAndOpenPopup<T extends StructuredDataSignatureOption
return openStructuredDataSignaturePopup({ token, options }, provider);
}

function parseUnserializableBigIntValues(payload: any) {
function parseUnserializableBigIntValues(payload: StructuredDataSignaturePayload) {
const { message, domain } = payload;

if (typeof message.type === 'string' && typeof domain.type === 'string') {
// new readable types
return {
...payload,
message: serializeCV(message),
domain: serializeCV(domain),
} as Json;
}

// legacy types
return {
...payload,
message: bytesToHex(serializeCV(payload.message)),
domain: bytesToHex(serializeCV(payload.domain)),
};
message: bytesToHex(legacySerializeCV(message as LegacyClarityValue)),
domain: bytesToHex(legacySerializeCV(domain as LegacyTupleCV)),
} as Json;
}

// eslint-disable-next-line @typescript-eslint/require-await
Expand All @@ -49,9 +66,9 @@ export async function signStructuredMessage(options: StructuredDataSignatureRequ
};
return signPayload(payload, privateKey);
}
// Type casting `any` as payload contains non-serialisable content,
// such as `StacksNetwork`
return createUnsecuredToken(parseUnserializableBigIntValues(options));
return createUnsecuredToken(
parseUnserializableBigIntValues(options as StructuredDataSignaturePayload)
);
}

async function openStructuredDataSignaturePopup(
Expand Down
63 changes: 44 additions & 19 deletions packages/connect/src/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { AppConfig, UserSession } from '@stacks/auth';
import { bytesToHex, hexToBytes } from '@stacks/common';
import { StacksTestnet } from '@stacks/network';
import { ChainId } from '@stacks/network';
import {
ChainID,
deserializeTransaction,
PostCondition,
postConditionToHex,
serializeCV,
serializePostCondition,
} from '@stacks/transactions';
import {
PostCondition as LegacyPostCondition,
serializeCV as legacySerializeCV,
serializePostCondition as legacySerializePostCondition,
} from '@stacks/transactions-v6';
import { createUnsecuredToken, Json, SECP256K1Client, TokenSigner } from 'jsontokens';
import { StacksProvider } from '../types';
import {
ContractCallOptions,
ContractCallPayload,
Expand All @@ -31,8 +36,7 @@ import {
TransactionPopup,
TransactionTypes,
} from '../types/transactions';
import { getStacksProvider } from '../utils';
import { StacksProvider } from '../types';
import { getStacksProvider, legacyNetworkFromConnectNetwork } from '../utils';

// TODO extract out of transactions
export const getUserSession = (_userSession?: UserSession) => {
Expand Down Expand Up @@ -64,21 +68,23 @@ export const getKeys = (_userSession?: UserSession) => {

// TODO extract out of transactions
export function getStxAddress(options: TransactionOptions) {
const { stxAddress, userSession, network } = options;
const { stxAddress, userSession, network: _network } = options;

if (stxAddress) return stxAddress;
if (!userSession || !network) return undefined;
if (!userSession || !_network) return undefined;
const stxAddresses = userSession?.loadUserData().profile?.stxAddress;

const chainIdToKey = {
[ChainID.Mainnet]: 'mainnet',
[ChainID.Testnet]: 'testnet',
[ChainId.Mainnet]: 'mainnet',
[ChainId.Testnet]: 'testnet',
};
const network = legacyNetworkFromConnectNetwork(_network);
const address: string | undefined = stxAddresses?.[chainIdToKey[network.chainId]];
return address;
}

function getDefaults(options: TransactionOptions) {
const network = options.network || new StacksTestnet();
const network = legacyNetworkFromConnectNetwork(options.network);

const userSession = getUserSession(options.userSession);
const defaults: TransactionOptions = {
Expand All @@ -93,15 +99,19 @@ function getDefaults(options: TransactionOptions) {
};
}

function encodePostConditions(postConditions: PostCondition[]) {
return postConditions.map(pc => bytesToHex(serializePostCondition(pc)));
}

// eslint-disable-next-line @typescript-eslint/require-await
async function signPayload(payload: TransactionPayload, privateKey: string) {
let { postConditions } = payload;
if (postConditions && typeof postConditions[0] !== 'string') {
postConditions = encodePostConditions(postConditions as PostCondition[]);
if (postConditions && postConditions.length > 0 && typeof postConditions[0] !== 'string') {
if (typeof postConditions[0].type === 'string') {
// new readable types
postConditions = (postConditions as PostCondition[]).map(postConditionToHex);
} else {
// legacy types
postConditions = (postConditions as LegacyPostCondition[]).map(pc => {
return bytesToHex(legacySerializePostCondition(pc));
});
}
}
const tokenSigner = new TokenSigner('ES256k', privateKey);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
Expand All @@ -110,8 +120,16 @@ async function signPayload(payload: TransactionPayload, privateKey: string) {

function createUnsignedTransactionPayload(payload: Partial<TransactionPayload>) {
let { postConditions } = payload;
if (postConditions && typeof postConditions[0] !== 'string') {
postConditions = encodePostConditions(postConditions as PostCondition[]);
if (postConditions && postConditions.length > 0 && typeof postConditions[0] !== 'string') {
if (typeof postConditions[0].type === 'string') {
// new readable types
postConditions = (postConditions as PostCondition[]).map(postConditionToHex);
} else {
// legacy types
postConditions = (postConditions as LegacyPostCondition[]).map(pc => {
return bytesToHex(legacySerializePostCondition(pc));
});
}
}
return createUnsecuredToken({ ...payload, postConditions } as unknown as Json);
}
Expand Down Expand Up @@ -151,7 +169,13 @@ export const makeContractCallToken = async (options: ContractCallOptions) => {
if (typeof arg === 'string') {
return arg;
}
return bytesToHex(serializeCV(arg));
if (typeof arg.type === 'string') {
// new readable types
return serializeCV(arg);
}

// legacy types
return bytesToHex(legacySerializeCV(arg));
});
if (hasAppPrivateKey(userSession)) {
const { privateKey, publicKey } = getKeys(userSession);
Expand Down Expand Up @@ -250,6 +274,7 @@ async function generateTokenAndOpenPopup<T extends TransactionOptions>(
const token = await makeTokenFn({
...getDefaults(options),
...options,
network: legacyNetworkFromConnectNetwork(options.network), // ensure network is legacy compatible
} as T);
return openTransactionPopup({ token, options }, provider);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/connect/src/types/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserSession } from '@stacks/auth';
import { StacksNetwork } from '@stacks/network';

import { AuthOptions } from './auth';
import { ConnectNetwork } from './network';

// Taken from @scure/btc-signer
// https://github.com/paulmillr/scure-btc-signer
Expand All @@ -23,7 +23,7 @@ export type PsbtFinished = (data: PsbtData) => void;
export interface PsbtBase {
appDetails?: AuthOptions['appDetails'];
authOrigin?: string;
network?: StacksNetwork;
network?: ConnectNetwork;
onCancel?: PsbtCanceled;
onFinish?: PsbtFinished;
stxAddress?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './signature';
export * from './structuredDataSignature';
export * from './profile';
export * from './bitcoin';
export * from './network';
13 changes: 13 additions & 0 deletions packages/connect/src/types/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StacksNetwork } from '@stacks/network';
import { StacksNetwork as LegacyNetwork, StacksNetworkName } from '@stacks/network-v6';

/**
* ⚠️ Warning: The new Stacks.js v7 network type is still experimental.
*/
export type ConnectNetwork =
| StacksNetworkName
| LegacyNetwork
| StacksNetwork
| {
url: string /** @experimental For backwards compatibility to allow selecting user-defined network, if network URL exists in users wallet */;
};
4 changes: 2 additions & 2 deletions packages/connect/src/types/profile.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { UserSession } from '@stacks/auth';
import { StacksNetwork } from '@stacks/network';
import { AuthOptions } from './auth';
import { PublicPersonProfile } from '@stacks/profile';
import { ConnectNetwork } from './network';

export type ProfileUpdateFinished = (data: PublicPersonProfile) => void;
export type ProfileUpdateCanceled = () => void;

export interface ProfileUpdateBase {
appDetails?: AuthOptions['appDetails'];
authOrigin?: string;
network?: StacksNetwork;
network?: ConnectNetwork;
stxAddress?: string;
userSession?: UserSession;
onFinish?: ProfileUpdateFinished;
Expand Down
Loading

0 comments on commit 4374364

Please sign in to comment.