玩转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认证URLhttps://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 之旅!

如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!