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

Add buildMixinOneSafePaymentUri #298

Merged
merged 1 commit into from
Nov 27, 2023
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
70 changes: 45 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@ethersproject/providers": "^5.6.8",
"@noble/curves": "^1.2.0",
"@noble/hashes": "^1.3.2",
"@types/qs": "^6.9.10",
"axios": "1.6.0",
"axios-retry": "3.4.0",
"curve25519-js": "^0.0.4",
Expand All @@ -95,6 +96,7 @@
"nano-seconds": "^1.2.2",
"node-forge": "^1.3.1",
"pako": "^2.0.4",
"qs": "^6.11.2",
"serialize-javascript": "^6.0.0",
"uuid": "^9.0.0",
"ws": "^8.7.0"
Expand Down
14 changes: 14 additions & 0 deletions src/client/types/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,17 @@ export interface OutputFetchRequest {
user_id: string;
ids: string[];
}

export interface PaymentParams {
uuid?: string;
mainnetAddress?: string;
mixAddress?: string;
members?: string[];
threshold?: number;

asset?: string;
amount?: string;
memo?: string;
trace?: string;
returnTo?: string;
}
12 changes: 6 additions & 6 deletions src/client/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const MainAddressPrefix = 'XIN';
export const MixAddressPrefix = 'MIX';
export const MixAddressVersion = 2;

export const GetPublicFromMainnetAddress = (address: string) => {
export const getPublicFromMainnetAddress = (address: string) => {
try {
if (!address.startsWith(MainAddressPrefix)) return undefined;

Expand All @@ -24,14 +24,14 @@ export const GetPublicFromMainnetAddress = (address: string) => {
}
};

export const MainnetAddressFromPublic = (pubKey: Buffer) => {
export const getMainnetAddressFromPublic = (pubKey: Buffer) => {
const msg = Buffer.concat([Buffer.from(MainAddressPrefix), pubKey]);
const checksum = newHash(msg);
const data = Buffer.concat([pubKey, checksum.subarray(0, 4)]);
return `${MainAddressPrefix}${utils.base58.encode(data)}`;
};

export const MixAddressFromString = (address: string): MixAddress | undefined => {
export const parseMixAddress = (address: string): MixAddress | undefined => {
try {
if (!address.startsWith(MixAddressPrefix)) return undefined;

Expand Down Expand Up @@ -66,7 +66,7 @@ export const MixAddressFromString = (address: string): MixAddress | undefined =>
if (memberData.length === total * 64) {
for (let i = 0; i < total; i++) {
const pub = memberData.subarray(64 * i, 64 * (i + 1));
const addr = MainnetAddressFromPublic(Buffer.from(pub));
const addr = getMainnetAddressFromPublic(Buffer.from(pub));
members.push(addr);
}
return {
Expand All @@ -81,7 +81,7 @@ export const MixAddressFromString = (address: string): MixAddress | undefined =>
}
};

export const GetMixAddress = (ma: MixAddress): string => {
export const buildMixAddress = (ma: MixAddress): string => {
if (ma.members.length > 255) {
throw new Error(`invalid members length: ${ma.members.length}`);
}
Expand All @@ -97,7 +97,7 @@ export const GetMixAddress = (ma: MixAddress): string => {
if (addr.startsWith(MainAddressPrefix)) {
if (!type) type = 'xin';
if (type !== 'xin') throw new Error(`inconsistent address type`);
const pub = GetPublicFromMainnetAddress(addr);
const pub = getPublicFromMainnetAddress(addr);
if (!pub) throw new Error(`invalid mainnet address: ${addr}`);
memberData.push(pub);
} else {
Expand Down
42 changes: 38 additions & 4 deletions src/client/utils/safe.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import forge from 'node-forge';
import qs from 'qs';
import { validate, v4 } from 'uuid';
import { FixedNumber } from 'ethers';
import { GhostKey, GhostKeyRequest, MultisigTransaction, SafeTransactionRecipient, SafeUtxoOutput } from '../types';
import { GhostKey, GhostKeyRequest, MultisigTransaction, PaymentParams, SafeTransactionRecipient, SafeUtxoOutput } from '../types';
import { Input, Output } from '../../mvm/types';
import { Encoder, magic } from '../../mvm';
import { base64RawURLEncode } from './base64';
import { TIPBodyForSequencerRegister } from './tip';
import { GetMixAddress, GetPublicFromMainnetAddress } from './address';
import { getPublicFromMainnetAddress, buildMixAddress, parseMixAddress } from './address';
import { encodeScript } from './multisigs';
import { blake3Hash, newHash, sha512Hash } from './uniq';
import { edwards25519 as ed } from './ed25519';
Expand All @@ -14,6 +16,38 @@ export const TxVersionHashSignature = 0x05;
export const OutputTypeScript = 0x00;
export const OutputTypeWithdrawalSubmit = 0xa1;

/**
* Build Payment Uri on https://mixin.one
* Destination can be set with
* 1. uuid: uuid of the Mixin user or bot
* 2. mainnetAddress: Mixin mainnet address started with "XIN"
* 3. mixAddress: address encoded with members and threshold and started with "MIX"
* 4. members and threshold: multisigs members' uuid or mainnet address, and threshold
*/
export const buildMixinOneSafePaymentUri = (params: PaymentParams) => {
let address = '';
if (params.uuid && validate(params.uuid)) address = params.uuid;
else if (params.mainnetAddress && getPublicFromMainnetAddress(params.mainnetAddress)) address = params.mainnetAddress;
else if (params.mixAddress && parseMixAddress(params.mixAddress)) address = params.mixAddress;
else if (params.members && params.threshold) {
address = buildMixAddress({
members: params.members,
threshold: params.threshold,
});
} else throw new Error('fail to get payment destination address');

const baseUrl = `https://mixin.one/pay/${address}`;
const p = {
asset: params.asset,
amount: params.amount,
memo: params.memo,
trace: params.trace ?? v4(),
return_to: params.returnTo && encodeURIComponent(params.returnTo),
};
const query = qs.stringify(p);
return `${baseUrl}?${query}`;
};

export const signSafeRegistration = (user_id: string, tipPin: string, privateKey: Buffer) => {
const public_key = forge.pki.ed25519.publicKeyFromPrivateKey({ privateKey }).toString('hex');

Expand Down Expand Up @@ -50,7 +84,7 @@ export const getMainnetAddressGhostKey = (recipient: GhostKeyRequest, hexSeed =
if (recipient.receivers.length === 0) return undefined;
if (hexSeed && hexSeed.length !== 128) return undefined;

const publics = recipient.receivers.map(d => GetPublicFromMainnetAddress(d));
const publics = recipient.receivers.map(d => getPublicFromMainnetAddress(d));
if (!publics.every(p => !!p)) return undefined;

const seed = hexSeed ? Buffer.from(hexSeed, 'hex') : Buffer.from(forge.random.getBytesSync(64), 'binary');
Expand All @@ -71,7 +105,7 @@ export const buildSafeTransactionRecipient = (members: string[], threshold: numb
members,
threshold,
amount,
mixAddress: GetMixAddress({ members, threshold }),
mixAddress: buildMixAddress({ members, threshold }),
});

export const getUnspentOutputsForRecipients = (outputs: SafeUtxoOutput[], rs: SafeTransactionRecipient[]) => {
Expand Down
Loading