玩转Sui多签钱包2:zkLogin公钥多签钱包 @SUI Move开发必知必会
本系列文章将会分为3部分内容,包括:
zkLogin
公钥多签钱包本文是该系列的第二篇,在正式介绍之前,我们先了解一些zkLogin
的知识。更详细的可参考:
zkLogin
是Sui
的原语(primitive
),它使你能够使用OAuth
凭证从Sui
地址发送交易,而无需公开将两者关联起来。
zkLogin
使你能够使用熟悉的OAuth
登录流程在Sui
上进行交易,避免了记住私钥或助记词的烦恼zkLogin
交易需要用户通过标准的OAuth
登录流程进行批准,OAuth
提供商不能代表用户进行交易zkLogin
是一种双因子认证方案;发送交易需要一个最近的 OAuth
登录凭证和一个不由 OAuth
提供商管理的盐。攻击者即使攻破了 OAuth
账户,也无法从用户相应的 Sui
地址进行交易,除非他们单独攻破盐Sui
地址与其相应的OAuth
标识符关联起来Sui
地址的OAuth
标识符。这为可验证的链上身份层奠定了基础zkLogin
是多种原生 Sui
签名方案之一,这得益于 Sui
的密码学灵活性。它与其他 Sui
原语(如赞助交易sponsored transactions
和多重签名multisig
)集成在一起zkLogin
的代码已由两家专门从事零知识的公司独立审计。创建公共参考字符串的公共 zkLogin 仪式吸引了来自 100 多名参与者的贡献OAuth
登录流程JWT
后JWT
获取唯一的用户盐值,并计算zkLogin Sui
地址Provider | Can support? | Devnet | Testnet | Mainnet |
---|---|---|---|---|
Yes | Yes | Yes | Yes | |
Yes | Yes | Yes | Yes | |
Twitch | Yes | Yes | Yes | Yes |
Slack | Yes | Yes | No | No |
Kakao | Yes | Yes | No | No |
Apple | Yes | Yes | Yes | Yes |
(1)用户本地生成临时密钥对(eph_sk, eph_pk
),并制定max_epoch
(2)将eph_pk
、到期时间(max_epoch
)和随机数 (jwt_randomness
)拼接后计算其哈希作为nonce
值后,发起登录操作
(3)用户完成 OAuth
登录流程后,可以在应用程序的重定向 URL
中得到 JWT
(4)应用前端发送JWT
到盐值服务上获取盐值
(5)盐值服务在校验JWT
通过后,会基于 iss, aud, sub
创建唯一的用户盐值 user_salt
,并返回
(6)用户向零知识证明服务发送JWT
、用户盐值、临时公钥、JWT
随机数、密钥声明名称(即:sub
)。证明服务生成一个零知识证明,将这些作为私有输入,并执行以下操作:
JWT
中的相应字段匹配JWT
上 OP
的 RSA
签名(7)返回零知识证明信息
(8)应用前端根据iss
、aud
、sub
计算用户地址。只要应用程序具有有效的 JWT
,就可以独立完成此步骤
(9)用户创建交易,使用临时私钥对交易进行签名,以生成临时签名
(10)用户将交易连通临时签名、临时公钥、零知识证明等信息提交给Sui
节点。Sui
节点会根据存储的 JWK
验证零知识证明以及临时签名是否正确
我们创建3个谷歌的OAuth
认证URL
,并分别在浏览器访问并登录,获得对应JWT
。其他OpenId
提供厂商的方法类似。
运行以下代码,将会创建3个谷歌OpenId OAuth
认证URL
,并将临时密钥、JWT随机数记录到.evn
文件中,便于后续使用。
import dotenv from "dotenv";
dotenv.config();
import { generateNonce, generateRandomness } from "@mysten/zklogin";
import { Ed25519Keypair, Ed25519PublicKey } from "@mysten/sui/keypairs/ed25519";
import { SUI_CLIENT } from "./suiClient";
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import * as fs from "fs";
const OPENID_AUTHORIZATION_ENDPOINT =
process.env.OPENID_AUTHORIZATION_ENDPOINT!;
const CLIENT_ID = process.env.CLIENT_ID!;
const REDIRECT_URL = process.env.REDIRECT_URL!;
// 创建临时密钥对,记录到.env中
const ekp1 = createEphemeralKeyPair("SK1");
const ekp2 = createEphemeralKeyPair("SK2");
const ekp3 = createEphemeralKeyPair("SK3");
// 本地生成随机数,记录到.env中
const jwtRandomness1 = createJWTRandomness("JWT_RANDOMNESS1");
const jwtRandomness2 = createJWTRandomness("JWT_RANDOMNESS2");
const jwtRandomness3 = createJWTRandomness("JWT_RANDOMNESS3");
function createJWTRandomness(tag: string): string {
const jwtRandomness = generateRandomness();
fs.appendFile(".env", `\n${tag}=${jwtRandomness}`, (err) => {
if (err) {
console.error(`append ${tag} jwtRandomness to .env failed, ${err}`);
return;
}
});
return jwtRandomness;
}
function createEphemeralKeyPair(tag: string): Ed25519Keypair {
const ekp = new Ed25519Keypair();
const skBytes = decodeSuiPrivateKey(ekp.getSecretKey()).secretKey.toString();
fs.appendFile(".env", `\n${tag}=${skBytes}`, (err) => {
if (err) {
console.error(`append ${tag} sk to .env failed, ${err}`);
return;
}
});
return ekp;
}
async function getOAuthURL(
epk: Ed25519PublicKey,
jwtRandomness: string
): Promise<string> {
const { epoch } = await SUI_CLIENT.getLatestSuiSystemState();
// 设置临时密钥对的过期时间(2天后过期),并添加到环境变量
const maxEpoch = Number(epoch) + 2;
fs.appendFile(".env", `\nMAX_EPOCH=${maxEpoch}`, (err) => {
if (err) {
console.error(`append MAX_EPOCH to .env failed, ${err}`);
return;
}
});
const nonce = generateNonce(epk, maxEpoch, jwtRandomness);
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URL,
response_type: "id_token",
scope: "openid email",
nonce: nonce,
});
return `${OPENID_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
}
async function main() {
const oAuthUrl1 = await getOAuthURL(ekp1.getPublicKey(), jwtRandomness1);
const oAuthUrl2 = await getOAuthURL(ekp2.getPublicKey(), jwtRandomness2);
const oAuthUrl3 = await getOAuthURL(ekp3.getPublicKey(), jwtRandomness3);
console.log(oAuthUrl1);
console.log(oAuthUrl2);
console.log(oAuthUrl3);
}
main();
OAuth
认证URL
https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080&response_type=id_token&scope=openid+email&nonce=lNj6vKFVTZOSMKczQlpspRL6isQ
https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080&response_type=id_token&scope=openid+email&nonce=TPquW9sogteqoIFoj8f0qfFbNfI
https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080&response_type=id_token&scope=openid+email&nonce=uaAkFzy7xd9g19u8zqiZVQbnTQg
将得到的
JWT
添加到.env
环境变量文件中,方便后续使用
JWT1=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2Fj~~~~~~wMTBhIn0.LU6JY3apvKpMa3HTqxTtf2kgMCiAVDHxE124yvUqoCQfrGFr1-_00hcAKi4sWbsiM2K7GzZrKAAuiEab9VA3KfkSw5Qw0W62ZH-bUT-vNkXECSfTWBXPnrf_w-cgWqRgIyzgPpmZM1p2W9A3swP9JwnJXjYetK63QtVTTm7xHBnW6PVMNTLechX1K3A6HsmNFYGsLK044PTeeP_tzdn6-9FilcxeD6fbQZlexlEyP41G4aVttI1oZHuudeifD5jC0gU5UP5H19Q1gr7QW8oZ4jMLVbPAlEfchr2KqGK0_w6AgCek5hYUEEjI16MVIdD16kQg42QIICre1GUk0aP6hA
JWT2=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291b~~~~~~ZThmOTRiNmY2MzIifQ.eRzpNjgwM8aw43_QcThS9gyWpgxT7jsbYeCtKpBakCHnaq9Bt3LS2m6enFfSmtrPy3SBRY7M3Lq8hCwtTfHsHoJz9Qyrj6ekXyWYj_EUHzkGKaA49fOzfNw1zWMryJ9YYkRt2W4b2ia1pQJZNfnbwEON8S1Mq9nmlXRaY2EvBPpQSwOEA-Y7E2Njtxpgcrz-OF87ms2ge3T1AU4N658Uz8bczsSIQLV6syfSWYFUcPuAKxmi-D-SZB2Bjq8bVL-nLDzgjf2nKkRBWUUDDRpcW9t7E7R9pQUcgUwnE_C23wev-73k_OMjJeN440TYxwHWzdIctHpUOae1gEjiCdA0cw
JWT3=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczov~~~~~~ZWY5OTE2ZWUifQ.DsNPdHATvhKw4qWCwuDnkePZIwBZHiR9CphyY8b8-yn9Q-L7ZZ_uYj15l0kGw-YUWXo6GkHBz90gNZ2aXvg09N6Zeq5COUz1KM40n6mmDVi1O1zoB57c28AkX0IriNiRY86p_TxCJ_f6CGW0u73kxINXiRzJ0_mSHGCf-iDTLJ63ncLoHjgL__YrJ-6Cdd9kGRMCQpIhL8KCJQza3JjXf1lHgtC53PamIWM5k5vNbkFJM7c1K8-Shr6qmfIFh7kpkWD49jun9bf9AseNBiQcR2bAdqFNqp2aEjlm1bZIzRR6iAHzgco1ShYtPp0GezxkjK2QvYUN7R_WRQI7QsorlQ
以下代码同样将创建三个zkLogin
公钥的多签钱包,其中公钥1和公钥2的权重为1,公钥3的权重为2,阈值为2。即:发起一笔交易,要么私钥1和私钥2进行zkLogin
多签、要么私钥3进行zkLogin
单签才能成功。
import dotenv from "dotenv";
dotenv.config();
import { genAddressSeed } from "@mysten/zklogin";
import { MultiSigPublicKey } from "@mysten/sui/multisig";
import {
ZkLoginPublicIdentifier,
toZkLoginPublicIdentifier,
} from "@mysten/sui/zklogin";
import { jwtDecode } from "jwt-decode";
// 从环境变量中获得的JWT
const JWT1 = process.env.JWT1 as string;
const JWT2 = process.env.JWT2 as string;
const JWT3 = process.env.JWT3 as string;
interface JwtPayload {
iss: string;
sub: string;
aud: string[] | string;
exp?: number;
nbf?: number;
iat?: number;
jti?: string;
}
function getZkLoginPk(jwt: string, userSalt: string): ZkLoginPublicIdentifier {
const decodedJWT = jwtDecode(jwt) as JwtPayload;
const addressSeed = genAddressSeed(
userSalt,
"sub",
decodedJWT.sub,
decodedJWT.aud.toString()
);
return toZkLoginPublicIdentifier(addressSeed, decodedJWT.iss);
}
function createMultisigAddressWithZKLogin(): MultiSigPublicKey {
const multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({
threshold: 2,
publicKeys: [
{
publicKey: getZkLoginPk(JWT1, "1001"),
weight: 1,
},
{
publicKey: getZkLoginPk(JWT2, "1002"),
weight: 1,
},
{
publicKey: getZkLoginPk(JWT3, "1003"),
weight: 2,
},
],
});
return multiSigPublicKey;
}
const multiSigPublicKey = createMultisigAddressWithZKLogin();
console.log(multiSigPublicKey.toSuiAddress());
为得到的多签钱包地址,进行充值,以便后续发送交易支付手续费。
运行以下代码,将会生成三个zkLogin
地址的零知识证明,将会用于生成最终的zkLogin
签名。
import dotenv from "dotenv";
dotenv.config();
import { getExtendedEphemeralPublicKey } from "@mysten/zklogin";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { getZkLoginSignature } from "@mysten/sui/zklogin";
import axios from "axios";
const PROVER_URL = process.env.PROVER_URL!;
function createEphemeralKeyPairFromEnv(tag: string): Ed25519Keypair {
let sk = process.env.SK1 as string;
if (tag == "SK2") {
sk = process.env.SK2 as string;
} else if (tag == "SK3") {
sk = process.env.SK3 as string;
}
const skArray = sk.split(",").map(Number);
return Ed25519Keypair.fromSecretKey(new Uint8Array(skArray));
}
// 从环境变量创建临时密钥对
const ekp1 = createEphemeralKeyPairFromEnv("SK1");
const ekp2 = createEphemeralKeyPairFromEnv("SK2");
const ekp3 = createEphemeralKeyPairFromEnv("SK3");
// 从环境变量获得随机数
const jwtRandomness1 = process.env.JWT_RANDOMNESS1 as string;
const jwtRandomness2 = process.env.JWT_RANDOMNESS2 as string;
const jwtRandomness3 = process.env.JWT_RANDOMNESS3 as string;
// 从环境变量中获得的JWT
const JWT1 = process.env.JWT1 as string;
const JWT2 = process.env.JWT2 as string;
const JWT3 = process.env.JWT3 as string;
async function getPartialZkLoginSignature(
keyPair: Ed25519Keypair,
jwt: string,
jwtRandomness: string,
userSalt: string
): Promise<any> {
const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(
keyPair.getPublicKey()
);
const verificationPayload = {
jwt: jwt,
extendedEphemeralPublicKey,
maxEpoch: process.env.MAX_EPOCH as string,
jwtRandomness: jwtRandomness,
salt: userSalt,
keyClaimName: "sub",
};
return await verifyPartialZkLoginSignature(verificationPayload);
}
export type PartialZkLoginSignature = Omit<
Parameters<typeof getZkLoginSignature>["0"]["inputs"],
"addressSeed"
>;
async function verifyPartialZkLoginSignature(zkpRequestPayload: any) {
try {
const proofResponse = await axios.post(PROVER_URL, zkpRequestPayload, {
headers: {
"content-type": "application/json",
},
});
const partialZkLoginSignature =
proofResponse.data as PartialZkLoginSignature;
return partialZkLoginSignature;
} catch (error) {
console.log("failed to reqeust the partial sig: ", error);
return {};
}
}
async function main() {
const partialZkLoginSignature1 = await getPartialZkLoginSignature(
ekp1,
JWT1,
jwtRandomness1,
"1001"
);
console.log(
"partialZkLoginSignature1: ",
JSON.stringify(partialZkLoginSignature1)
);
const partialZkLoginSignature2 = await getPartialZkLoginSignature(
ekp2,
JWT2,
jwtRandomness2,
"1002"
);
console.log(
"partialZkLoginSignature2: ",
JSON.stringify(partialZkLoginSignature2)
);
const partialZkLoginSignature3 = await getPartialZkLoginSignature(
ekp3,
JWT3,
jwtRandomness3,
"1003"
);
console.log(
"partialZkLoginSignature3: ",
JSON.stringify(partialZkLoginSignature3)
);
}
main();
partialZkLoginSignature1: {"proofPoints":{"a":["14832536503620236750207539878309821304798845102886328200500002064261861024596","6098458781316679357431194461835045775756847359647150465987882945922602097684","1"],"b":[["8526710911098951542164729912098087801943066291645009567420356653275589013339","12191499139322446608204189789140884435811809808585535318587642087068460430650"],["15157126251392138080206919153527847755110528209517287687180723390366575294684","18435843692752302986435461580737708077786738415333681364131368985540376512131"],["1","0"]],"c":["9915061636399225528628085890086021346545174008425467121122025394538813828858","4234817782411751794097725246812494469567246413958518347403982701380724289799","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}
partialZkLoginSignature2: {"proofPoints":{"a":["18433919245915507148114493793411264105374103943871562990223521930266836271983","18939976090943115419839770923844241604814364406395842871698008772206087436878","1"],"b":[["11944915040664030630589443644466915837266322696718148988840305616669987862064","7700565435072459510735266829091297288471998405565419014882510487612437556381"],["15360253758166950243890078560825748794737606051799398911095255605626063841827","21330394914721293545198183304724898397231260614444624014943854049670139090199"],["1","0"]],"c":["11441443481802854899856472972901858079195788325079011273495325063938702195236","11852191467263750050627671811533142604227756537216910900889732585930256826158","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}
partialZkLoginSignature3: {"proofPoints":{"a":["4740731145292925652215230270851732536502954743960527232699771677642762080376","3047046322893722166104080614047142015224179441494873249302137668013525987808","1"],"b":[["19889686587291816871051044555672650377990875240267055150536208167747114908713","2676334022512023736718118063511178797092366686119862357043349874865738569290"],["9132265628905091888922925613035178934071346230407134478683778927653892962291","13794186112643208431435349588679025537267304717941663769161943409075541251871"],["1","0"]],"c":["18109617987645336494129514748177200680204598861839472849880675856081829298271","7177148781916594569772897974686503609852049585577400096543979831550750303275","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}
这里发送的交易还是采用我们简单的笔记合约交易,具体请参看:
由于用户1权重为1,多签钱包阈值为2,所以用户1单签名交易,交易验签会不通过,将会报错。
async function main() {
await addNote("欢迎关注微信公众号:Move中文", "开启你的Sui Move之旅!");
}
async function addNote(title: string, body: string) {
const txb = new Transaction();
const txData = {
target: `${PACKAGE_ID}::notes::create_note`,
arguments: [txb.pure.string(title), txb.pure.string(body)],
};
return makeMoveCall(txData, txb);
}
async function makeMoveCall(txData: any, txb: Transaction) {
const sender = getWalletAddress();
console.log("sender: ", sender);
txb.setSender(sender);
txb.moveCall(txData);
// 写法1
const txBytes = await txb.build({ client: SUI_CLIENT });
const userSignature1 = (await ekp1.signTransaction(txBytes)).signature;
// 写法2
// const { bytes, signature: userSignature1 } = await txb.sign({
// client: SUI_CLIENT,
// signer: ekp1,
// });
const partialZkLoginSignature1 = await getPartialZkLoginSignature(
ekp1,
JWT1,
jwtRandomness1,
"1001"
);
const zkLoginSignature1 = await generateZkLoginSignature(
partialZkLoginSignature1,
"1001",
JWT1,
userSignature1
);
const combinedSignature = multiSigPublicKey.combinePartialSignatures([
zkLoginSignature1,
]);
return SUI_CLIENT.executeTransactionBlock({
transactionBlock: txBytes,
signature: zkLoginSignature1,
});
}
报错信息为:Signature is not valid: Insufficient weight=1 threshold=2
,符合预期。
/xxx/node_modules/@mysten/sui/src/client/http-transport.ts:119
throw new JsonRpcError(data.error.message, data.error.code);
^
JsonRpcError: Invalid user signature: Signature is not valid: Insufficient weight=1 threshold=2
at SuiHTTPTransport.request (/xxx/node_modules/@mysten/sui/src/client/http-transport.ts:119:10)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at async SuiClient.executeTransactionBlock (/xxx/node_modules/@mysten/sui/src/client/client.ts:414:10) {
code: -32002,
type: 'TransactionExecutionClientError'
}
注:上面代码实现,即便是单签,也必须使用combinePartialSignatures
,否则会报类似如下错:
JsonRpcError: Invalid user signature: Required Signature from 0x06ebec847702fbc9f6de8487cf8cffcdd3739c0a957b08bda9ccc09ba8f6d7a8 is absent ["0x32b21e34a7ca68f14312f81304c29e87f385337ffeeadda927a06468926dc5f1"]
由于用户1和用户2的权重均为1,多签钱包阈值为2,当用户1和用户2进行多签时,达到阀值2,交易可以执行成功。
async function main() {
await addNote("欢迎关注微信公众号:Move中文", "开启你的Sui Move之旅!");
}
async function addNote(title: string, body: string) {
const txb = new Transaction();
const txData = {
target: `${PACKAGE_ID}::notes::create_note`,
arguments: [txb.pure.string(title), txb.pure.string(body)],
};
return makeMoveCall(txData, txb);
}
async function makeMoveCall(txData: any, txb: Transaction) {
const sender = getWalletAddress();
console.log("sender: ", sender);
txb.setSender(sender);
txb.moveCall(txData);
const txBytes = await txb.build({ client: SUI_CLIENT });
const userSignature1 = (await ekp1.signTransaction(txBytes)).signature;
const userSignature2 = (await ekp2.signTransaction(txBytes)).signature;
const partialZkLoginSignature1 = await getPartialZkLoginSignature(
ekp1,
JWT1,
jwtRandomness1,
"1001"
);
const partialZkLoginSignature2 = await getPartialZkLoginSignature(
ekp2,
JWT2,
jwtRandomness2,
"1002"
);
const zkLoginSignature1 = await generateZkLoginSignature(
partialZkLoginSignature1,
"1001",
JWT1,
userSignature1
);
const zkLoginSignature2 = await generateZkLoginSignature(
partialZkLoginSignature2,
"1002",
JWT2,
userSignature2
);
const combinedSignature = multiSigPublicKey.combinePartialSignatures([
zkLoginSignature1,
zkLoginSignature2,
]);
return SUI_CLIENT.executeTransactionBlock({
transactionBlock: txBytes,
signature: combinedSignature,
});
}
查询浏览器,从交易签名中,我们可以看到是用户1和用户2的zkLogin
地址进行了多签,交易成功执行。
由于用户3权重为2,多签钱包阈值为2,估只需要用户3单签名就能达到阀值2,交易便可以执行成功。
async function main() {
await addNote("欢迎关注微信公众号:Move中文", "开启你的Sui Move之旅!");
}
async function addNote(title: string, body: string) {
const txb = new Transaction();
const txData = {
target: `${PACKAGE_ID}::notes::create_note`,
arguments: [txb.pure.string(title), txb.pure.string(body)],
};
return makeMoveCall(txData, txb);
}
async function makeMoveCall(txData: any, txb: Transaction) {
const sender = getWalletAddress();
console.log("sender: ", sender);
txb.setSender(sender);
txb.moveCall(txData);
const txBytes = await txb.build({ client: SUI_CLIENT });
const userSignature3 = (await ekp3.signTransaction(txBytes)).signature;
const partialZkLoginSignature3 = await getPartialZkLoginSignature(
ekp3,
JWT3,
jwtRandomness3,
"1003"
);
const zkLoginSignature3 = await generateZkLoginSignature(
partialZkLoginSignature3,
"1003",
JWT3,
userSignature3
);
const combinedSignature = multiSigPublicKey.combinePartialSignatures([
zkLoginSignature3,
]);
return SUI_CLIENT.executeTransactionBlock({
transactionBlock: txBytes,
signature: combinedSignature,
});
查询浏览器,从交易签名中,我们可以看到只有用户3的zkLogin
地址进行了单签,交易成功。
https://sdk.mystenlabs.com/typescript/cryptography/multisig
本文示例中创建多签钱包使用的三个公钥均为zkLogin
公钥,也可以混合使用普通公钥和zkLogin
公钥生成多签钱包地址,以满足不同场景下的需求。
欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!