Skip to content

Commit e4dc5d3

Browse files
GTC6244claude
andauthored
feat: return groupId when adding groups (CPL-145) (#202)
* feat: return group_id from addGroup across contract and API (CPL-145) Modify the Solidity addGroup function to return the on-chain group ID (uint256). Update ABI, generated Rust bindings, and make send_transaction generic over Detokenize. The Rust add_group function simulates the call first to capture the group ID, then sends the real transaction. Adds AddGroupResponse with success + group_id fields. Includes a fix for signer pool leak if the simulation call fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: surface group_id in SDK and dashboard (CPL-145) Update core_sdk.js addGroup to document the new AddGroupResponse type. Dashboard now shows the created group ID in the success toast. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: use returned group_id in K6 tests (CPL-145) Add AddGroupResponse type to K6 client. Integration and security tests now extract group_id directly from the addGroup response instead of calling listGroups, simplifying setup and removing race conditions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update API examples with addGroup response format (CPL-145) Show the new {"success":true,"group_id":"1"} response in API docs. Remove the now-unnecessary listGroups step from quickstart examples. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fe210af commit e4dc5d3

15 files changed

Lines changed: 85 additions & 71 deletions

File tree

docs/api_direct.mdx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,8 @@ Create a group (with optional permitted actions and PKPs). Then add an action (I
156156
-H "Content-Type: application/json" \
157157
-H "X-Api-Key: KEY" \
158158
-d '{"group_name":"My Group","group_description":"","pkp_ids_permitted":[],"cid_hashes_permitted":[]}'
159-
160-
# List groups to get group_id (use the "id" from the response)
161-
curl -s "http://localhost:8000/core/v1/list_groups?page_number=0&page_size=10" \
162-
-H "X-Api-Key: KEY"
163-
159+
# Response: {"success":true,"group_id":"1"}
160+
164161
# Register action metadata (replace IPFS_CID)
165162
curl -s -X POST "http://localhost:8000/core/v1/add_action" \
166163
-H "Content-Type: application/json" \

docs/management/api_direct.mdx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,9 @@ Create a group, then add an action (IPFS CID) to scope which keys can run it.
175175
"pkp_ids_permitted": [],
176176
"cid_hashes_permitted": []
177177
}'
178+
# Response: {"success":true,"group_id":"1"}
178179

179-
# List groups to get group_id
180-
curl -s "http://localhost:8000/core/v1/list_groups?page_number=0&page_size=10" \
181-
-H "X-Api-Key: KEY"
182-
183-
# Add action to group (replace GROUP_ID and IPFS_CID)
180+
# Add action to group (use group_id from the response above)
184181
curl -s -X POST "http://localhost:8000/core/v1/add_action_to_group" \
185182
-H "Content-Type: application/json" \
186183
-H "X-Api-Key: KEY" \

k6/correctness/api-key-security.spec.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -130,23 +130,15 @@ export function setup(): SecuritySetupData {
130130
adminA,
131131
);
132132
if (!assertOk("setup/addGroupX", "POST /add_group", addGroupXRes)) throw new Error("setup failed: addGroupX");
133+
const groupIdX = parseInt((addGroupXRes.data as { group_id: string }).group_id);
133134

134135
// Create group Y
135136
const addGroupYRes = client.addGroup(
136137
{ group_name: `k6-sec-groupY-${K6_RUN_ID}`, group_description: "Security test group Y", pkp_ids_permitted: [], cid_hashes_permitted: [] },
137138
adminA,
138139
);
139140
if (!assertOk("setup/addGroupY", "POST /add_group", addGroupYRes)) throw new Error("setup failed: addGroupY");
140-
141-
// List groups to extract IDs
142-
const listGroupsRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
143-
if (!assertOk("setup/listGroups", "GET /list_groups", listGroupsRes)) throw new Error("setup failed: listGroups");
144-
const groups = listGroupsRes.data as Array<{ id: string; name: string }>;
145-
const groupX = groups.find((g) => g.name === `k6-sec-groupX-${K6_RUN_ID}`);
146-
const groupY = groups.find((g) => g.name === `k6-sec-groupY-${K6_RUN_ID}`);
147-
if (!groupX || !groupY) throw new Error("setup failed: could not find created groups");
148-
const groupIdX = parseInt(groupX.id);
149-
const groupIdY = parseInt(groupY.id);
141+
const groupIdY = parseInt((addGroupYRes.data as { group_id: string }).group_id);
150142

151143
// Get IPFS CID for hello-world action
152144
const ipfsRes = client.getLitActionIpfsId(HELLO_WORLD_CODE);
@@ -218,13 +210,7 @@ export function setup(): SecuritySetupData {
218210
adminB,
219211
);
220212
if (!assertOk("setup/addGroupB", "POST /add_group", addGroupBRes)) throw new Error("setup failed: addGroupB");
221-
222-
const listGroupsBRes = client.listGroups({ page_number: 0, page_size: 100 }, adminB);
223-
if (!assertOk("setup/listGroupsB", "GET /list_groups", listGroupsBRes)) throw new Error("setup failed: listGroupsB");
224-
const groupsB = listGroupsBRes.data as Array<{ id: string; name: string }>;
225-
const groupB = groupsB.find((g) => g.name === `k6-sec-groupB-${K6_RUN_ID}`);
226-
if (!groupB) throw new Error("setup failed: could not find Account B group");
227-
const groupIdB = parseInt(groupB.id);
213+
const groupIdB = parseInt((addGroupBRes.data as { group_id: string }).group_id);
228214

229215
// Add action to Account B's group
230216
const addActionBRes = client.addAction(
@@ -290,13 +276,9 @@ export default function (data: SecuritySetupData) {
290276
assertOk("2-createGroup-positive", "POST /add_group", posRes);
291277

292278
// Clean up: delete the created group with admin key
293-
const listRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
294-
if (assertOk("2-listGroups-cleanup", "GET /list_groups", listRes)) {
295-
const gs = listRes.data as Array<{ id: string; name: string }>;
296-
const created = gs.find((g) => g.name === `k6-sec-t2-pos-${K6_RUN_ID}`);
297-
if (created) {
298-
client.removeGroup({ group_id: created.id }, adminA);
299-
}
279+
const createdGroupId = (posRes.data as { group_id: string }).group_id;
280+
if (createdGroupId) {
281+
client.removeGroup({ group_id: createdGroupId }, adminA);
300282
}
301283

302284
// Negative: key with can_create_groups=false
@@ -315,15 +297,11 @@ export default function (data: SecuritySetupData) {
315297
adminA,
316298
);
317299
assertOk("3-createTempGroup", "POST /add_group", tempRes);
318-
const listRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
319-
assertOk("3-listGroups", "GET /list_groups", listRes);
320-
const gs = listRes.data as Array<{ id: string; name: string }>;
321-
const temp = gs.find((g) => g.name === `k6-sec-t3-temp-${K6_RUN_ID}`);
322-
if (!temp) { console.error("3-deleteGroup: temp group not found"); return; }
300+
const tempGroupId = (tempRes.data as { group_id: string }).group_id;
323301

324302
// Positive: key with can_delete_groups=true
325303
const posRes = client.removeGroup(
326-
{ group_id: temp.id },
304+
{ group_id: tempGroupId },
327305
authHeaders(data.usageKey_deleteGroups),
328306
);
329307
assertOk("3-deleteGroup-positive", "POST /remove_group", posRes);

k6/correctness/integration.spec.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,16 @@ export default function (data: IntegrationSetupData) {
130130
checkAndLog(addGroupRes.response, {
131131
"addGroup success": (r) => {
132132
try {
133-
return JSON.parse(r.body as string).success === true;
133+
const body = JSON.parse(r.body as string);
134+
return body.success === true && typeof body.group_id === "string";
134135
} catch {
135136
return false;
136137
}
137138
},
138139
}, "addGroup");
140+
const groupId = (addGroupRes.data as { success: boolean; group_id: string }).group_id;
139141

140-
// ── 7. listGroups — extract groupId for subsequent tests ──────────────────
142+
// ── 7. listGroups — verify group appears ──────────────────────────────────
141143
const listGroupsRes = client.listGroups(
142144
{ page_number: 0, page_size: 10 },
143145
authHeaders,
@@ -152,12 +154,6 @@ export default function (data: IntegrationSetupData) {
152154
}
153155
},
154156
}, "listGroups");
155-
const groups = listGroupsRes.data as Array<{ id: string; name: string; description: string }>;
156-
if (!groups || groups.length === 0) {
157-
console.error("listGroups returned empty array after addGroup");
158-
return;
159-
}
160-
const groupId = groups[groups.length - 1].id; // use the most recently created group
161157

162158
// ── 8. updateGroup — called while group is empty so the full-replace is safe ───
163159
const updateGroupRes = client.updateGroup(

k6/litApiServer.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,20 @@ export interface LitActionRequest {
7272
}
7373

7474
/**
75-
* Response for account config operations (add_group, add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
75+
* Response for account config operations (add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
7676
*/
7777
export interface AccountOpResponse {
7878
success: boolean;
7979
}
8080

81+
/**
82+
* Response for add_group, includes the on-chain group ID.
83+
*/
84+
export interface AddGroupResponse {
85+
success: boolean;
86+
group_id: string;
87+
}
88+
8189
/**
8290
* Request for add_group. permitted_actions and pkps are keccak256 hashes as hex strings (with or without 0x). API key via header.
8391
*/
@@ -398,7 +406,7 @@ export type AddGroupHeaders = {
398406
"X-Api-Key": string;
399407
};
400408

401-
export type AddGroupDefault = AccountOpResponse | ErrMessage;
409+
export type AddGroupDefault = AddGroupResponse | ErrMessage;
402410

403411
export type RemoveGroupHeaders = {
404412
/**

lit-api-server/blockchain/lit_node_express/contracts/AccountConfigFacets/WritesFacet.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ contract WritesFacet {
107107
string memory description,
108108
uint256[] memory cidHashes,
109109
address[] memory pkpIds
110-
) public {
110+
) public returns (uint256) {
111111
SecurityLib.revertIfNoAccountAccess(accountApiKeyHash, msg.sender);
112112
SecurityLib.revertIfNotMasterAccount(accountApiKeyHash);
113113
AppStorage.AccountConfigStorage storage s = AppStorage.getStorage();
@@ -124,6 +124,7 @@ contract WritesFacet {
124124
for (uint256 i = 0; i < pkpIds.length; i++) {
125125
group.pkpId.add(pkpIds[i]);
126126
}
127+
return account.groupCount;
127128
}
128129

129130
function updateGroup(

lit-api-server/src/accounts/contracts/AccountConfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,13 @@
215215
}
216216
],
217217
"name": "addGroup",
218-
"outputs": [],
218+
"outputs": [
219+
{
220+
"internalType": "uint256",
221+
"name": "",
222+
"type": "uint256"
223+
}
224+
],
219225
"stateMutability": "nonpayable",
220226
"type": "function"
221227
},

lit-api-server/src/accounts/contracts/account_config_contract.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2230,7 +2230,7 @@ pub mod account_config {
22302230
description: ::std::string::String,
22312231
cid_hashes: ::std::vec::Vec<::ethers::core::types::U256>,
22322232
pkp_ids: ::std::vec::Vec<::ethers::core::types::Address>,
2233-
) -> ::ethers::contract::builders::ContractCall<M, ()> {
2233+
) -> ::ethers::contract::builders::ContractCall<M, ::ethers::core::types::U256> {
22342234
self.0
22352235
.method_hash(
22362236
[43, 18, 222, 230],

lit-api-server/src/accounts/mod.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,37 @@ pub async fn add_group(
7373
description: &str,
7474
cid_hashes: Vec<U256>,
7575
pkp_ids: Vec<Address>,
76-
) -> Result<bool> {
76+
) -> Result<U256> {
7777
let (contract, signer_address, client) =
7878
get_signable_account_config_contract(signer_pool.clone()).await?;
7979
let account_api_key_hash = api_key_hash(api_key);
80+
81+
// Simulate the call first to obtain the returned group ID.
82+
let sim_call = contract.add_group(
83+
account_api_key_hash,
84+
name.to_string(),
85+
description.to_string(),
86+
cid_hashes.clone(),
87+
pkp_ids.clone(),
88+
);
89+
let group_id = match sim_call.call().await {
90+
Ok(id) => id,
91+
Err(e) => {
92+
// Release the signer back to the pool before propagating.
93+
signer_pool.release(signer_address).await?;
94+
return Err(e.into());
95+
}
96+
};
97+
8098
let function_call = contract.add_group(
8199
account_api_key_hash,
82100
name.to_string(),
83101
description.to_string(),
84102
cid_hashes,
85103
pkp_ids,
86104
);
87-
send_transaction(function_call, signer_pool, signer_address, client).await
105+
send_transaction(function_call, signer_pool, signer_address, client).await?;
106+
Ok(group_id)
88107
}
89108

90109
/// Create a new action entry with name, description, and IPFS CID hash in the account's actionMetadata mapping.

lit-api-server/src/accounts/signable_contract.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ pub(crate) async fn get_read_only_account_config_contract()
108108
Ok(contract)
109109
}
110110

111-
pub async fn send_transaction(
112-
mut function_call: ContractCall<SigningClient, ()>,
111+
pub async fn send_transaction<T: ethers::abi::Detokenize>(
112+
mut function_call: ContractCall<SigningClient, T>,
113113
signer_pool: Arc<SignerPool>,
114114
signer_address: H160,
115115
client: Arc<SigningClient>,

0 commit comments

Comments
 (0)