lodash#Dictionary TypeScript Examples

The following examples show how to use lodash#Dictionary. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts    From yfm-transform with MIT License 7 votes vote down vote up
export function errorToString(path: string, error: LintError, sourceMap?: Dictionary<string>) {
    const ruleMoniker = error.ruleNames
        ? error.ruleNames.join(sep)
        : // @ts-expect-error bad markdownlint typings
          error.ruleName + sep + error.ruleAlias;
    const lineNumber = sourceMap ? sourceMap[error.lineNumber] : error.lineNumber;

    return (
        `${path}${lineNumber ? `: ${lineNumber}:` : ':'} ${ruleMoniker} ${error.ruleDescription}` +
        (error.errorDetail ? ` [${error.errorDetail}]` : '') +
        (error.errorContext ? ` [Context: "${error.errorContext}"]` : '')
    );
}
Example #2
Source File: traceConvert.ts    From erda-ui with GNU Affero General Public License v3.0 6 votes vote down vote up
createSpanTreeEntry = (
  trace: MONITOR_TRACE.ITraceSpan,
  traces: MONITOR_TRACE.ITraceSpan[],
  indexByParentId?: Dictionary<MONITOR_TRACE.ITraceSpan[]>,
): Entry => {
  const idx =
    indexByParentId ||
    fp.flow(
      fp.filter((s: MONITOR_TRACE.ITraceSpan) => s.parentSpanId !== ''),
      fp.groupBy((s: MONITOR_TRACE.ITraceSpan) => s.parentSpanId),
    )(traces);

  return {
    span: trace,
    children: (idx[trace.id] || []).map((s: MONITOR_TRACE.ITraceSpan) => createSpanTreeEntry(s, traces, idx)),
  };
}
Example #3
Source File: ExplicitLoaderImpl.ts    From type-graphql-dataloader with MIT License 6 votes vote down vote up
function directLoader<V>(
  relation: RelationMetadata,
  connection: Connection,
  grouper: string | ((entity: V) => any)
) {
  return async (ids: readonly any[]) => {
    const entities = keyBy(
      await connection
        .createQueryBuilder<V>(relation.type, relation.propertyName)
        .whereInIds(ids)
        .getMany(),
      grouper
    ) as Dictionary<V>;
    return ids.map((id) => entities[id]);
  };
}
Example #4
Source File: index.tsx    From roam-toolkit with MIT License 6 votes vote down vote up
ReactHotkeys = ({keyMap, handlers}: Props) => {
    /**
     * Key sequences like 'g g' mess up the other shortcuts
     * See https://github.com/greena13/react-hotkeys/issues/229
     * And https://github.com/greena13/react-hotkeys/issues/219
     *
     * Workaround by separating sequences and single chords into different react components:
     * https://github.com/greena13/react-hotkeys/issues/219#issuecomment-540680435
     */
    const hotkeys: Dictionary<Hotkey> = mapValues(
        zipObjects(keyMap, handlers),
        ([keySequenceString, handler]): Hotkey => [KeySequence.fromString(keySequenceString), handler]
    )

    const singleChordHotkeys = pickBy(hotkeys, usesOneKeyChord)
    const multiChordHotkeys = pickBy(hotkeys, usesMultipleKeyChords)

    return (
        <>
            <GlobalHotKeysWithoutConflictingWithNativeHotkeys
                keyMap={mapValues(singleChordHotkeys, toKeySequence)}
                handlers={mapValues(singleChordHotkeys, toHandler)}
            />
            <GlobalHotKeysWithoutConflictingWithNativeHotkeys
                keyMap={mapValues(multiChordHotkeys, toKeySequence)}
                handlers={mapValues(multiChordHotkeys, toHandler)}
            />
        </>
    )
}
Example #5
Source File: object.ts    From roam-toolkit with MIT License 6 votes vote down vote up
zipObjects = <X, Y>(xs: Dictionary<X>, ys: Dictionary<Y>): Dictionary<[X, Y]> =>
    Object.keys(xs).reduce((keyToXY, key) => {
        const x = xs[key]
        const y = ys[key]
        if (x && y) {
            return {
                ...keyToXY,
                [key]: [x, y],
            }
        }
        return keyToXY
    }, {})
Example #6
Source File: TagProvider.tsx    From dashboard with Apache License 2.0 6 votes vote down vote up
TagProvider: React.FC = (props) => {
  const [allTags, setAllTags] = React.useState<MaterialTag.Item[]>([])
  const [tagsTimestamp, setTagsItemsTimestamp] = React.useState(Date.now);
  const [allTagsMap, setAllTagsMap] = React.useState<Dictionary<MaterialTag.Item>>({})
  const getTagList = (name?: string) => {
    QueryMaterialLibraryTags({page_size: 5000, name}).then((res) => {
      if (res?.code === 0) {
        setAllTags(res?.data?.items)
        setAllTagsMap(_.keyBy<MaterialTag.Item>(res?.data?.items, 'id'))
      } else {
        message.error(res?.message);
      }
    })
  }
  React.useEffect(() => {
    getTagList()
  }, [tagsTimestamp])
  return (
    <TagContext.Provider value={{allTags, setAllTags, setTagsItemsTimestamp, allTagsMap}}>
      {props.children}
    </TagContext.Provider>
  )
}
Example #7
Source File: traceConvert.ts    From erda-ui with GNU Affero General Public License v3.0 6 votes vote down vote up
recursiveGetRootMostSpan = (
  idSpan: Dictionary<MONITOR_TRACE.ITraceSpan>,
  prevSpan: MONITOR_TRACE.ITraceSpan,
): MONITOR_TRACE.ITraceSpan => {
  if (prevSpan.parentSpanId && idSpan[prevSpan.parentSpanId]) {
    return recursiveGetRootMostSpan(idSpan, idSpan[prevSpan.parentSpanId]);
  }
  return prevSpan;
}
Example #8
Source File: contract.ts    From balancer-v2-monorepo with GNU General Public License v3.0 6 votes vote down vote up
// From https://github.com/nomiclabs/hardhat/issues/611#issuecomment-638891597, temporary workaround until
// https://github.com/nomiclabs/hardhat/issues/1716 is addressed.
function linkBytecode(artifact: Artifact, libraries: Dictionary<string>): string {
  let bytecode = artifact.bytecode;

  for (const [, fileReferences] of Object.entries(artifact.linkReferences)) {
    for (const [libName, fixups] of Object.entries(fileReferences)) {
      const addr = libraries[libName];
      if (addr === undefined) {
        continue;
      }

      for (const fixup of fixups) {
        bytecode =
          bytecode.substr(0, 2 + fixup.start * 2) +
          addr.substr(2) +
          bytecode.substr(2 + (fixup.start + fixup.length) * 2);
      }
    }
  }

  return bytecode;
}
Example #9
Source File: KubernetesFetcher.ts    From backstage with Apache License 2.0 6 votes vote down vote up
function fetchResultsToResponseWrapper(
  results: FetchResult[],
): FetchResponseWrapper {
  const groupBy: Dictionary<FetchResult[]> = lodash.groupBy(results, value => {
    return isError(value) ? 'errors' : 'responses';
  });

  return {
    errors: groupBy.errors ?? [],
    responses: groupBy.responses ?? [],
  } as FetchResponseWrapper; // TODO would be nice to get rid of this 'as'
}
Example #10
Source File: upload-hobo-data.ts    From aqualink-app with MIT License 5 votes vote down vote up
parseHoboData = async (
  poiEntities: SiteSurveyPoint[],
  dbIdToCSVId: Record<number, number>,
  rootPath: string,
  poiToSourceMap: Dictionary<Sources>,
  timeSeriesRepository: Repository<TimeSeries>,
) => {
  // Parse hobo data
  const parsedData = poiEntities.map((poi) => {
    const colonyId = poi.name.split(' ')[1].padStart(3, '0');
    const dataFile = COLONY_DATA_FILE.replace('{}', colonyId);
    const colonyFolder = COLONY_FOLDER_PREFIX + colonyId;
    const siteFolder = FOLDER_PREFIX + dbIdToCSVId[poi.site.id];
    const filePath = path.join(rootPath, siteFolder, colonyFolder, dataFile);
    const headers = [undefined, 'id', 'dateTime', 'bottomTemperature'];
    const castFunction = castCsvValues(
      ['id'],
      ['bottomTemperature'],
      ['dateTime'],
    );
    return parseCSV<Data>(filePath, headers, castFunction).map((data) => ({
      timestamp: data.dateTime,
      value: data.bottomTemperature,
      source: poiToSourceMap[poi.id],
      metric: Metric.BOTTOM_TEMPERATURE,
    }));
  });

  // Find the earliest date of data
  const startDates = parsedData.reduce((acc, data) => {
    const minimum = minBy(data, (o) => o.timestamp);

    if (!minimum) {
      return acc;
    }

    return acc.concat(minimum);
  }, []);

  const groupedStartedDates = keyBy(startDates, (o) => o.source.site.id);

  // Start a backfill for each site
  const siteDiffDays: [number, number][] = Object.keys(groupedStartedDates).map(
    (siteId) => {
      const startDate = groupedStartedDates[siteId];
      if (!startDate) {
        return [parseInt(siteId, 10), 0];
      }

      const start = moment(startDate.timestamp);
      const end = moment();
      const diff = Math.min(end.diff(start, 'd'), 200);

      return [startDate.source.site.id, diff];
    },
  );

  const bottomTemperatureData = parsedData.flat();

  // Data are to much to added with one bulk insert
  // So we need to break them in batches
  const batchSize = 1000;
  logger.log(`Saving time series data in batches of ${batchSize}`);
  const inserts = chunk(bottomTemperatureData, batchSize).map((batch) => {
    return timeSeriesRepository
      .createQueryBuilder('time_series')
      .insert()
      .values(batch)
      .onConflict('ON CONSTRAINT "no_duplicate_data" DO NOTHING')
      .execute();
  });

  // Return insert promises and print progress updates
  const actionsLength = inserts.length;
  await Bluebird.Promise.each(inserts, (props, idx) => {
    logger.log(`Saved ${idx + 1} out of ${actionsLength} batches`);
  });

  return siteDiffDays;
}
Example #11
Source File: index.ts    From yfm-transform with MIT License 5 votes vote down vote up
function liquid<
    B extends boolean = false,
    C = B extends false ? string : {output: string; sourceMap: Dictionary<string>},
>(
    originInput: string,
    vars: Record<string, unknown>,
    path?: string,
    settings?: ArgvSettings & {withSourceMap?: B},
): C {
    const {
        cycles = true,
        conditions = true,
        substitutions = true,
        conditionsInCode = false,
        keepNotVar = false,
        withSourceMap,
    } = settings || {};

    ArgvService.init({
        cycles,
        conditions,
        substitutions,
        conditionsInCode,
        keepNotVar,
        withSourceMap,
    });

    let output = conditionsInCode ? originInput : saveCode(originInput, vars, path, substitutions);

    let sourceMap: Record<number, number> = {};

    if (withSourceMap) {
        const lines = output.split('\n');
        sourceMap = lines.reduce((acc: Record<number, number>, _cur, index) => {
            acc[index + 1] = index + 1;

            return acc;
        }, {});
    }

    if (cycles) {
        output = applyCycles(output, vars, path, {sourceMap});
    }

    if (conditions) {
        output = applyConditions(output, vars, path, {sourceMap});
    }

    if (substitutions) {
        output = applySubstitutions(output, vars, path);
    }

    output = conditionsInCode ? output : repairCode(output);
    codes.length = 0;

    if (withSourceMap) {
        return {
            output,
            sourceMap: prepareSourceMap(sourceMap),
        } as unknown as C;
    }

    return output as unknown as C;
}
Example #12
Source File: BacklinksTreeDataProvider.ts    From dendron with GNU Affero General Public License v3.0 5 votes vote down vote up
private shallowFirstPathSort(
    referencesByPath: Dictionary<[unknown, ...unknown[]]>
  ) {
    return sortPaths(Object.keys(referencesByPath), {
      shallowFirst: true,
    });
  }
Example #13
Source File: check-video-streams.ts    From aqualink-app with MIT License 5 votes vote down vote up
checkVideoOptions = (youTubeVideoItems: YouTubeVideoItem[]) =>
  youTubeVideoItems.reduce<Dictionary<string>>((mapping, item) => {
    return {
      ...mapping,
      [item.id]: getErrorMessage(item),
    };
  }, {})
Example #14
Source File: CustomResources.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
kindToResource = (customResources: any[]): Dictionary<any[]> => {
  return lodash.groupBy(customResources, value => {
    return value.kind;
  });
}
Example #15
Source File: index.ts    From roam-toolkit with MIT License 5 votes vote down vote up
Features = {
    all: prepareSettings([
        incDec, // prettier
        srs,
        blockManipulation,
        vimMode,
        spatialMode,
        estimate,
        navigation,
        dateTitle,
        fuzzyDate,
        livePreview,
        randomPage,
    ]),

    isActive: Settings.isActive,

    async getActiveFeatures(): Promise<Feature[]> {
        return filterAsync(this.all, it => this.isActive(it.id))
    },

    getShortcutHandlers: (): Dictionary<Handler> =>
        getAllShortcuts(Features.all).reduce((acc: any, current) => {
            acc[current.id] = current.onPress
            return acc
        }, {}),

    async getCurrentKeyMap(): Promise<Dictionary<KeySequenceString>> {
        const features = (await Features.getActiveFeatures()).filter(it => it.settings)
        const allShortcuts = (await mapAsync(features, this.getKeyMapFor)).flat().filter(it => it[1])
        return allShortcuts.reduce((acc: any, current) => {
            acc[current[0]] = current[1]
            return acc
        }, {})
    },

    async getKeyMapFor(feature: Feature) {
        return mapAsync(feature.settings!, async it => [it.id, await Settings.get(feature.id, it.id)])
    },
}
Example #16
Source File: upload-hobo-data.ts    From aqualink-app with MIT License 5 votes vote down vote up
createSurveyPoints = async (
  siteEntities: Site[],
  dbIdToCSVId: Record<number, number>,
  recordsGroupedBySite: Dictionary<Coords[]>,
  rootPath: string,
  poiRepository: Repository<SiteSurveyPoint>,
) => {
  // Create site points of interest entities for each imported site
  // Final result needs to be flattened since the resulting array is grouped by site
  const surveyPoints = siteEntities
    .map((site) => {
      const currentSiteId = dbIdToCSVId[site.id];
      const siteFolder = FOLDER_PREFIX + currentSiteId;
      return recordsGroupedBySite[currentSiteId]
        .filter((record) => {
          const colonyId = record.colony.toString().padStart(3, '0');
          const colonyFolder = COLONY_FOLDER_PREFIX + colonyId;
          const colonyFolderPath = path.join(
            rootPath,
            siteFolder,
            colonyFolder,
          );

          return fs.existsSync(colonyFolderPath);
        })
        .map((record) => {
          const point: Point | undefined =
            !isNaN(record.long) && !isNaN(record.lat)
              ? createPoint(record.long, record.lat)
              : undefined;

          return {
            name: COLONY_PREFIX + record.colony,
            site,
            polygon: point,
          };
        });
    })
    .flat();

  logger.log('Saving site points of interest');
  const poiEntities = await Promise.all(
    surveyPoints.map((poi) =>
      poiRepository
        .save(poi)
        .catch(handleEntityDuplicate(poiRepository, poiQuery, poi.polygon)),
    ),
  );

  return poiEntities;
}
Example #17
Source File: decorator.ts    From nestjs-paginate with MIT License 5 votes vote down vote up
Paginate = createParamDecorator((_data: unknown, ctx: ExecutionContext): PaginateQuery => {
    const request: Request = ctx.switchToHttp().getRequest()
    const { query } = request

    // Determine if Express or Fastify to rebuild the original url and reduce down to protocol, host and base url
    let originalUrl
    if (request.originalUrl) {
        originalUrl = request.protocol + '://' + request.get('host') + request.originalUrl
    } else {
        originalUrl = request.protocol + '://' + request.hostname + request.url
    }
    const urlParts = new URL(originalUrl)
    const path = urlParts.protocol + '//' + urlParts.host + urlParts.pathname

    const sortBy: [string, string][] = []
    const searchBy: string[] = []

    if (query.sortBy) {
        const params = !Array.isArray(query.sortBy) ? [query.sortBy] : query.sortBy
        for (const param of params) {
            if (isString(param)) {
                const items = param.split(':')
                if (items.length === 2) {
                    sortBy.push(items as [string, string])
                }
            }
        }
    }

    if (query.searchBy) {
        const params = !Array.isArray(query.searchBy) ? [query.searchBy] : query.searchBy
        for (const param of params) {
            if (isString(param)) {
                searchBy.push(param)
            }
        }
    }

    const filter = mapKeys(
        pickBy(
            query,
            (param, name) =>
                name.includes('filter.') &&
                (isString(param) || (Array.isArray(param) && (param as any[]).every((p) => isString(p))))
        ) as Dictionary<string | string[]>,
        (_param, name) => name.replace('filter.', '')
    )

    return {
        page: query.page ? parseInt(query.page.toString(), 10) : undefined,
        limit: query.limit ? parseInt(query.limit.toString(), 10) : undefined,
        sortBy: sortBy.length ? sortBy : undefined,
        search: query.search ? query.search.toString() : undefined,
        searchBy: searchBy.length ? searchBy : undefined,
        filter: Object.keys(filter).length ? filter : undefined,
        path,
    }
})
Example #18
Source File: form.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerMassMsgForm: React.FC<CustomerMassMsgFormProps> = (props) => {
  const {initialValues} = props;
  const staffSelectRef = useRef<HTMLInputElement>(null);
  const [selectedStaffs, setSelectedStaffs] = useState<StaffOption[]>([]);
  const [staffSelectionVisible, setStaffSelectionVisible] = useState(false);
  const [isFetchDone, setIsFetchDone] = useState(false);
  const [massMsg, setMassMsg] = useState<Msg>({text: ''});
  const [allTagGroups, setAllTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [allGroupChats, setAllGroupChats] = useState<GroupChatOption[]>([]);
  const [tagLogicalCondition, setTagLogicalCondition] = useState<'and' | 'or' | 'none'>('or');
  const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
  const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>();

  useEffect(() => {
    QuerySimpleStaffs({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllStaffs(res?.data?.items || []);
        setStaffMap(_.keyBy<any>(res?.data?.items, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);


  useEffect(() => {
    QueryCustomerTagGroups({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllTagGroups(res?.data?.items);
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QueryGroupChatMainInfo({page_size: 5000}).then((res: CommonResp) => {
      if (res.code === 0) {
        setAllGroupChats(
          res.data?.items?.map((item: GroupChatMainInfoItem) => {
            return {
              key: item.ext_chat_id,
              label: item.name,
              value: item.ext_chat_id,
              ...item,
            };
          }).filter((item: { name: any; }) => item.name),
        );
      } else {
        message.error(res.message);
      }
    });
  }, []);


  const itemDataToFormValues = (item: CustomerMassMsgItem | any): CreateCustomerMassMsgParam => {
    let values: CreateCustomerMassMsgParam = {
      ...item,
      id: item.id,
      ext_staff_ids: item?.staffs?.map((staff: any) => staff.ext_id),
    };

    if (item?.ext_staff_ids) {
      setSelectedStaffs(
        item?.ext_staff_ids.map((ext_staff_id: string) => {
          let staffInfo = {};
          if (staffMap && staffMap[ext_staff_id]) {
            staffInfo = staffMap[ext_staff_id]
          }
          return {
            ...staffInfo,
            key: ext_staff_id,
            value: ext_staff_id,
          };
        }),
      );
    }

    if (item.msg) {
      setMassMsg(item.msg);
    }

    values = {
      ...values, ...{
        gender: item?.ext_customer_filter?.gender,
        date_range: [item?.ext_customer_filter?.start_time, item?.ext_customer_filter?.end_time],
        ext_group_chat_ids: item?.ext_customer_filter?.ext_group_chat_ids,
        ext_department_ids: item?.ext_customer_filter?.ext_department_ids,
        ext_tag_ids: item?.ext_customer_filter?.ext_tag_ids,
        tag_logical_condition: item?.ext_customer_filter?.tag_logical_condition,
        exclude_ext_tag_ids: item?.ext_customer_filter?.exclude_ext_tag_ids,
      }
    };

    return values;
  };

  useEffect(() => {
    if (initialValues?.id) {
      props?.formRef?.current?.setFieldsValue(itemDataToFormValues(initialValues));
      setIsFetchDone(true);
    }
  }, [initialValues, staffMap]);

  const formatParams = (values: any) => {
    const params: CreateCustomerMassMsgParam = {
      id: initialValues?.id || '',
      chat_type: 'single',
      ext_customer_filter_enable: values?.ext_customer_filter_enable,
      ext_department_ids: [],
      ext_staff_ids: selectedStaffs.map((staff) => staff.ext_id) || [],
      msg: massMsg,
      send_at: values?.send_at,
      send_type: values?.send_type,
    };
    if (params.ext_customer_filter_enable === Enable) {
      params.ext_customer_filter = {
        gender: values?.gender,
        start_time: values?.da,
        end_time: values?.end_time,
        ext_group_chat_ids: values?.ext_group_chat_ids,
        ext_department_ids: values?.ext_department_ids,
        ext_tag_ids: values?.ext_tag_ids,
        tag_logical_condition: tagLogicalCondition,
        exclude_ext_tag_ids: values?.exclude_ext_tag_ids,
      };

      if (values?.date_range) {
        [params.ext_customer_filter.start_time, params.ext_customer_filter.end_time] = values?.date_range;
      }
    }
    return params;
  };

  const checkForm = (values: any): boolean => {
    if (values?.ext_staff_ids?.length === 0) {
      message.warning('请选择使用员工');
      staffSelectRef?.current?.focus();
      return false;
    }
    if (values?.msg?.text === '') {
      message.warning('请填写群发内容');
      return false;
    }
    return true;
  };

  // @ts-ignore
  return (
    <>
      <ProForm
        className={styles.content}
        labelCol={{
          md: 3,
        }}
        layout={'horizontal'}
        formRef={props.formRef}
        onFinish={async (values: any) => {
          const params = formatParams(values);
          if (!checkForm(params)) {
            return false;
          }
          console.log(params);
          return props.onFinish(params);
        }}
      >
        <>
          <h3>基础信息</h3>
          <Divider/>
          <Alert
            showIcon={true}
            style={{maxWidth: '800px', marginBottom: 20}}
            type='warning'
            message={(
              <Typography.Text style={{color: 'rgba(66,66,66,0.8)'}}>客户每个月最多接收来自同一企业的管理员的 4
                条群发消息,4条消息可在同一天发送</Typography.Text>
            )}
          />

          <ProFormRadio.Group
            name='send_type'
            label='群发时机'
            initialValue={1}
            options={[
              {
                label: '立即发送',
                value: 1,
              },
              {
                label: '定时发送',
                value: 2,
              },
            ]}
            rules={[
              {
                required: true,
                message: '请选择群发时机',
              },
            ]}
          />

          <ProFormDependency name={['send_type']}>
            {({send_type}) => {
              if (send_type === 2) {
                return (
                  <ProFormDateTimePicker
                    name='send_at'
                    label='发送时间'
                    rules={[
                      {
                        required: true,
                        message: '请选择发送时间',
                      },
                    ]}
                  />
                );
              }
              return '';
            }}
          </ProFormDependency>

          <Form.Item
            label={<span className={'form-item-required'}>群发账号</span>}
          >
            <Button
              ref={staffSelectRef}
              icon={<PlusOutlined/>}
              onClick={() => setStaffSelectionVisible(true)}
            >
              选择员工
            </Button>
          </Form.Item>

          <Space wrap={true} style={{marginTop: -12, marginBottom: 24, marginLeft: 20}}>
            {selectedStaffs.map((item) => (
              <div key={item.id} className={'staff-item'}>
                <Badge
                  count={
                    <CloseCircleOutlined
                      onClick={() => {
                        setSelectedStaffs(
                          selectedStaffs.filter((staff) => staff.id !== item.id),
                        );
                      }}
                      style={{color: 'rgb(199,199,199)'}}
                    />
                  }
                >
                  <div className={'container'}>
                    <img alt={item.name} className={'avatar'} src={item.avatar_url}/>
                    <span className={'text'}>{item.name}</span>
                  </div>
                </Badge>
              </div>
            ))}
          </Space>

          <ProFormRadio.Group
            name='ext_customer_filter_enable'
            label='目标客户'
            initialValue={2}
            options={[
              {
                label: '全部客户',
                value: Disable,
              },
              {
                label: '筛选客户',
                value: Enable,
              },
            ]}
            rules={[
              {
                required: true,
                message: '请选择目标客户',
              },
            ]}
          />
          <ProFormDependency name={['ext_customer_filter_enable']}>
            {({ext_customer_filter_enable}) => {
              if (ext_customer_filter_enable === Enable) {
                return (
                  <div className={styles.multiFormItemSection}>
                    <ProFormRadio.Group
                      name='gender'
                      label='性别'
                      initialValue={0}
                      options={[
                        {
                          label: '全部',
                          value: 0,
                        },
                        {
                          label: '仅男粉丝',
                          value: 1,
                        },
                        {
                          label: '仅女粉丝',
                          value: 2,
                        },
                        {
                          label: '未知性别',
                          value: 3,
                        },
                      ]}
                    />

                    <ProForm.Item label={'所在群聊'} name={'ext_group_chat_ids'}>
                      <GroupChatSelect
                        placeholder={'请选择群聊'}
                        allGroupChats={allGroupChats}
                        maxTagCount={4}/>
                    </ProForm.Item>

                    <ProFormDateRangePicker name='date_range' label='添加时间'/>

                    <ProForm.Item label={'目标客户'} name={'ext_tag_ids'}>
                      <CustomerTagSelect
                        withLogicalCondition={true}
                        logicalCondition={tagLogicalCondition}
                        setLogicalCondition={setTagLogicalCondition}
                        placeholder={'按标签选择客户'}
                        allTagGroups={allTagGroups}
                        maxTagCount={4}/>
                    </ProForm.Item>

                    <ProForm.Item label={'排除客户'} name={'exclude_ext_tag_ids'}>
                      <CustomerTagSelect
                        placeholder={'按标签排除客户'}
                        allTagGroups={allTagGroups}
                        maxTagCount={4}/>
                    </ProForm.Item>

                  </div>
                );
              }
              return '';
            }}
          </ProFormDependency>

          <h3>群发设置</h3>
          <Divider/>

          <Form.Item
            label={<span className={'form-item-required'}>群发内容</span>}
            style={{marginBottom: 36}}
          >
            <AutoReply welcomeMsg={massMsg} isFetchDone={isFetchDone} setWelcomeMsg={setMassMsg}/>
          </Form.Item>

        </>
      </ProForm>

      <StaffTreeSelectionModal
        visible={staffSelectionVisible}
        setVisible={setStaffSelectionVisible}
        defaultCheckedStaffs={selectedStaffs}
        onFinish={(values) => {
          setSelectedStaffs(values);
        }}
        allStaffs={allStaffs}
      />
    </>
  );
}
Example #19
Source File: create-config.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export function createConfig(data: Record<string, unknown>) {
  const config = merge({}, unflatten(data, { delimiter: '_' })) as Record<
    string,
    unknown
  >;
  const $warnings: string[] = [];

  const configOutputFilePattern = String(
    config.outputFilePattern || `{model}/{name}.{type}.ts`,
  );

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  let outputFilePattern = filenamify(configOutputFilePattern, {
    replacement: '/',
  })
    .replace(/\.\./g, '/')
    .replace(/\/+/g, '/');
  outputFilePattern = trim(outputFilePattern, '/');

  if (outputFilePattern !== configOutputFilePattern) {
    $warnings.push(
      `Due to invalid filepath 'outputFilePattern' changed to '${outputFilePattern}'`,
    );
  }

  if (config.reExportAll) {
    $warnings.push(`Option 'reExportAll' is deprecated, use 'reExport' instead`);
    if (toBoolean(config.reExportAll)) {
      config.reExport = 'All';
    }
  }

  const fields: Record<string, ConfigFieldSetting | undefined> = Object.fromEntries(
    Object.entries<Dictionary<string | undefined>>(
      (config.fields ?? {}) as Record<string, Dictionary<string | undefined>>,
    )
      .filter(({ 1: value }) => typeof value === 'object')
      .map(([name, value]) => {
        const fieldSetting: ConfigFieldSetting = {
          arguments: [],
          output: toBoolean(value.output),
          input: toBoolean(value.input),
          model: toBoolean(value.model),
          from: value.from,
          defaultImport: toBoolean(value.defaultImport) ? true : value.defaultImport,
          namespaceImport: value.namespaceImport,
        };
        return [name, fieldSetting];
      }),
  );

  const decorate: DecorateElement[] = [];
  const configDecorate: (Record<string, string> | undefined)[] = Object.values(
    (config.decorate as any) || {},
  );

  for (const element of configDecorate) {
    if (!element) continue;
    ok(
      element.from && element.name,
      `Missed 'from' or 'name' part in configuration for decorate`,
    );
    decorate.push({
      isMatchField: outmatch(element.field, { separator: false }),
      isMatchType: outmatch(element.type, { separator: false }),
      from: element.from,
      name: element.name,
      namedImport: toBoolean(element.namedImport),
      defaultImport: toBoolean(element.defaultImport) ? true : element.defaultImport,
      namespaceImport: element.namespaceImport,
      arguments: element.arguments ? JSON5.parse(element.arguments) : undefined,
    });
  }

  return {
    outputFilePattern,
    tsConfigFilePath: createTsConfigFilePathValue(config.tsConfigFilePath),
    combineScalarFilters: toBoolean(config.combineScalarFilters),
    noAtomicOperations: toBoolean(config.noAtomicOperations),
    reExport: (ReExport[String(config.reExport)] || ReExport.None) as ReExport,
    emitSingle: toBoolean(config.emitSingle),
    emitCompiled: toBoolean(config.emitCompiled),
    $warnings,
    fields,
    purgeOutput: toBoolean(config.purgeOutput),
    useInputType: createUseInputType(config.useInputType as any),
    noTypeId: toBoolean(config.noTypeId),
    requireSingleFieldsInWhereUniqueInput: toBoolean(
      config.requireSingleFieldsInWhereUniqueInput,
    ),
    graphqlScalars: (config.graphqlScalars || {}) as Record<
      string,
      ImportNameSpec | undefined
    >,
    decorate,
  };
}
Example #20
Source File: EnterpriseScript.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
EnterpriseScript: (props: any, ref: any) => JSX.Element = (props, ref) => {
  const actionRef = useRef<ActionType>();
  const formRef = useRef({} as any);
  const [keyWord, setKeyWord] = useState('')
  const [groupId, setGroupId] = useState('')
  const [department_id, setDepartmentId] = useState<number[]>([])
  const [allDepartments, setAllDepartments] = useState<DepartmentOption[]>([]);
  const [allDepartmentMap, setAllDepartmentMap] = useState<Dictionary<DepartmentOption>>({});
  const [targetScriptGroup, setTargetScriptGroup] = useState<Partial<ScriptGroup.Item>>({})
  const [groupModalVisible, setGroupModalVisible] = useState(false);
  const groupModalRef = useRef<any>({});
  const scriptModalRef = useRef<any>({});
  const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
  const [scriptModalVisible, setScriptModalVisible] = useState(false);
  const [targetScript, setTargetScript] = useState<Partial<ScriptGroup.Item>>({})
  const [groupItems, setGroupItems] = useState<Partial<ScriptGroup.Item>[]>([]);
  const [allGroupMap, setAllGroupMap] = useState<Dictionary<ScriptGroup.Item>>({});
  const [selectedItems, setSelectedItems] = useState<Script.Item[]>([]);

  useImperativeHandle(ref, () => {
    return {
      createEnterpriseScript: () => {
        setTargetScript({})
        scriptModalRef.current.open({reply_details: [{content_type: 2}]})
      },
    }
  })
  useEffect(() => {
    QueryEnterpriseScriptGroups({page_size: 5000}).then(res => {
      if (res?.code === 0) {
        setGroupItems(res?.data?.items || [])
        setAllGroupMap(_.keyBy<ScriptGroup.Item>(res?.data?.items || [], 'id'));
      }
    }).catch((err) => {
      message.error(err);
    });
  }, [groupItemsTimestamp])

  useEffect(() => {
    QueryDepartmentList({page_size: 5000}).then((res) => {
      if (res?.code === 0) {
        const departments =
          res?.data?.items?.map((item: DepartmentInterface) => {
            return {
              label: item?.name,
              value: item?.ext_id,
              ...item,
            };
          }) || [];
        setAllDepartments(departments);
        setAllDepartmentMap(_.keyBy<DepartmentOption>(departments, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);

  // 后端数据转为前端组件FormItem -- name
  const transferParams = (paramsFromBackEnd: any) => {
    const newReplyDetails = []
    for (let i = 0; i < paramsFromBackEnd.reply_details.length; i += 1) {
      const typeObjectKey = typeEnums[paramsFromBackEnd.reply_details[i].content_type]
      const replyDetailItem: FrontEndReplyDetailParams = {
        content_type: paramsFromBackEnd.reply_details[i].content_type, id: paramsFromBackEnd.reply_details[i].id
      }
      const quickReplyContent = paramsFromBackEnd.reply_details[i].quick_reply_content
      if (typeObjectKey === 'text') {
        replyDetailItem.text_content = quickReplyContent.text.content
      }
      if (typeObjectKey === 'image') {
        replyDetailItem.image_title = quickReplyContent.image.title
        replyDetailItem.image_size = quickReplyContent.image.size
        replyDetailItem.image_picurl = quickReplyContent.image.picurl
      }
      if (typeObjectKey === 'link') {
        replyDetailItem.link_title = quickReplyContent.link.title
        replyDetailItem.link_desc = quickReplyContent.link.desc
        replyDetailItem.link_picurl = quickReplyContent.link.picurl
        replyDetailItem.link_url = quickReplyContent.link.url
      }
      if (typeObjectKey === 'pdf') {
        replyDetailItem.pdf_title = quickReplyContent.pdf.title
        replyDetailItem.pdf_size = quickReplyContent.pdf.size
        replyDetailItem.pdf_fileurl = quickReplyContent.pdf.fileurl
      }
      if (typeObjectKey === 'video') {
        replyDetailItem.video_title = quickReplyContent.video.title
        replyDetailItem.video_size = quickReplyContent.video.size
        replyDetailItem.video_picurl = quickReplyContent.video.picurl
      }
      newReplyDetails.push(replyDetailItem)
    }
    return {...paramsFromBackEnd, reply_details: newReplyDetails}
  }


  const columns: ProColumns<Script.Item>[] = [
    {
      title: '话术内容',
      dataIndex: 'keyword',
      width: '18%',
      hideInSearch: false,
      render: (dom: any, item: any) => {
        return (
          <ScriptContentPreView script={item}/>
        )
      },
    },
    {
      title: '标题',
      dataIndex: 'name',
      key:'name',
      valueType: 'text',
      hideInSearch: true,
    },
    {
      title: '发送次数',
      dataIndex: 'send_count',
      key: 'name',
      valueType: 'digit',
      hideInSearch: true,
      width: 100,
    },
    {
      title: '所属分组',
      width: '14%',
      dataIndex: 'group_id',
      key: 'group_id',
      valueType: 'text',
      hideInSearch: true,
      render: (dom: any) => {
        return (
          <span>{allGroupMap[dom]?.name || '-'}</span>
        )
      },
    },
    {
      title: '创建人',
      dataIndex: 'staff_name',
      key: 'staff_name',
      hideInSearch: true,
      render: (dom, item) => (
        <div className={'tag-like-staff-item'}>
          <img className={'icon'} src={item.avatar}/>
          <span className={'text'}>{dom}</span>
        </div>
      )
    },
    {
      title: '创建时间',
      dataIndex: 'created_at',
      key: 'created_at',
      valueType: 'dateTime',
      hideInSearch: true,
      render: (dom, item) => {
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: moment(item.created_at)
                .format('YYYY-MM-DD HH:mm')
                .split(' ')
                .join('<br />'),
            }}
          />
        );
      },
    },
    {
      title: '类型',
      dataIndex: 'quick_reply_type',
      key: 'quick_reply_type',
      valueType: 'text',
      hideInSearch: true,
      render: (dom: any) => {
        return <span>{typeEnums[dom]}</span>
      }
    },
    {
      title: '可用部门',
      dataIndex: 'department_id',
      key: 'department_id',
      valueType: 'text',
      hideInSearch: false,
      hideInTable: true,
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      renderFormItem: (schema, config, form) => {
        return (
          <DepartmentTreeSelect
            options={allDepartments}
          />
        )
      },
    },
    {
      title: '操作',
      valueType: 'text',
      width: '10%',
      hideInSearch: true,
      render: (dom) => {
        return (
          <Space>
            <Button
              type={'link'}
              onClick={() => {
                // @ts-ignore
                const target = transferParams(dom)
                setTargetScript(target)
                // @ts-ignore
                scriptModalRef.current.open(target)
              }}
            >修改</Button>
            <Button
              type={'link'}
              onClick={() => {
                Modal.confirm({
                  title: `删除话术`,
                  // @ts-ignore
                  content: `是否确认删除「${dom?.name}」话术?`,
                  okText: '删除',
                  okType: 'danger',
                  cancelText: '取消',
                  onOk() {
                    // @ts-ignore
                    return HandleRequest({ids: [dom?.id]}, DeleteEnterpriseScriptList, () => {
                      actionRef?.current?.reload()
                    })
                  },
                });
              }}
            >
              删除
            </Button>
          </Space>
        )
      }
    },
  ]
  const getUseableDepartment = (departments: number[]) => {
    return departments.map((id) => {
      return <span key={id}>
           {id === 0 ? '全部员工可见' : allDepartmentMap[id]?.label} &nbsp;
        </span>
    })
  }

  return (
    <>
      <ProTable
        onSubmit={() => {
          setKeyWord(formRef.current.getFieldValue('keyword'))
          setDepartmentId(formRef.current.getFieldValue('department_id'))
        }}
        onReset={() => {
          setKeyWord(formRef?.current?.getFieldValue('keyword'))
          setDepartmentId(formRef?.current?.getFieldValue('department_id'))
        }}
        actionRef={actionRef}
        formRef={formRef}
        className={'table'}
        scroll={{x: 'max-content'}}
        columns={columns}
        rowKey={(scriptItem) => scriptItem.id}
        pagination={{
          pageSizeOptions: ['5', '10', '20', '50', '100'],
          pageSize: 5,
        }}
        toolBarRender={false}
        bordered={false}
        tableAlertRender={false}
        rowSelection={{
          onChange: (__, items) => {
            setSelectedItems(items);
          },
        }}
        tableRender={(block, dom) => (
          <div className={styles.mixedTable}>
            <div className={styles.leftPart}>
              <div className={styles.header}>
                <Button
                  key="1"
                  className={styles.button}
                  type="text"
                  onClick={() => {
                    setTargetScriptGroup({})
                    groupModalRef.current.open({})
                  }}
                  icon={<PlusSquareFilled style={{color: 'rgb(154,173,193)', fontSize: 15}}/>}
                >
                  新建分组
                </Button>
              </div>
              <Menu
                onSelect={(e) => {
                  setGroupId(e.key as string)
                }}
                defaultSelectedKeys={['']}
                mode="inline"
                className={styles.menuList}
              >
                <Menu.Item
                  icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                  onClick={() => setTargetScriptGroup({})}
                  key=""
                >
                  全部
                </Menu.Item>
                {groupItems.map((item) => (
                  <Menu.Item
                    icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                    key={item.id}
                    onClick={() => {
                      setTargetScriptGroup(item)
                    }}
                  >
                    {item.name}
                    <Dropdown
                      className={'more-actions'}
                      overlay={
                        <Menu
                          onClick={(e) => {
                            e.domEvent.preventDefault();
                            e.domEvent.stopPropagation();
                          }}
                        >
                          <Menu.Item
                            onClick={() => {
                              setTargetScriptGroup(item)
                              groupModalRef.current.open(item)
                            }}
                            key="edit"
                          >
                            <a type={'link'}>修改分组</a>

                          </Menu.Item>
                          <Menu.Item
                            key="delete"
                          >
                            <a
                              type={'link'}
                              onClick={() => {
                                Modal.confirm({
                                  title: `删除分组`,
                                  // @ts-ignore
                                  content: `是否确认删除「${item?.name}」分组?`,
                                  okText: '删除',
                                  okType: 'danger',
                                  cancelText: '取消',
                                  onOk() {
                                    // @ts-ignore
                                    return HandleRequest({ids: [item.id]}, DeleteEnterpriseScriptGroups, () => {
                                      setGroupItemsTimestamp(Date.now)
                                    })
                                  },
                                });
                              }}
                            >
                              删除分组
                            </a>
                          </Menu.Item>
                        </Menu>
                      }
                      trigger={['hover']}
                    >
                      <MoreOutlined style={{color: '#9b9b9b', fontSize: 18}}/>
                    </Dropdown>

                  </Menu.Item>
                ))}

              </Menu>
            </div>
            <div className={styles.rightPart}>
              {
                targetScriptGroup?.id
                &&
                <div className={styles.aboveTableWrap}>
                  可见范围&nbsp;<Tooltip title={'范围内的员工可在企业微信【侧边栏】使用话术'}><QuestionCircleOutlined /></Tooltip>:
                  {allGroupMap[targetScriptGroup.id]?.departments ? getUseableDepartment(allGroupMap[targetScriptGroup.id]?.departments) : '全部员工可见'}
                  &nbsp;&nbsp;&nbsp;
                  <a onClick={() => groupModalRef.current.open(allGroupMap[targetScriptGroup!.id!])}>修改</a>
                </div>
              }
              <div className={styles.tableWrap}>{dom}</div>
            </div>
          </div>
        )}
        params={{
          group_id: groupId || '',
          department_ids: department_id || [],
          keyword: keyWord || ''
        }}
        request={async (params, sort, filter) => {
          return ProTableRequestAdapter(params, sort, filter, QueryEnterpriseScriptList);
        }}
        dateFormatter="string"
      />

      {selectedItems?.length > 0 && (
        // 底部选中条目菜单栏
        <FooterToolbar>
          <span>
            已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项 &nbsp;&nbsp;
          </span>
          <Divider type='vertical'/>
          <Button
            type='link'
            onClick={() => {
              actionRef.current?.clearSelected?.();
            }}
          >
            取消选择
          </Button>
          <Button
            icon={<DeleteOutlined/>}
            onClick={async () => {
              Modal.confirm({
                title: `删除渠道码`,
                content: `是否批量删除所选「${selectedItems.length}」个渠道码?`,
                okText: '删除',
                okType: 'danger',
                cancelText: '取消',
                onOk() {
                  return HandleRequest(
                    {ids: selectedItems.map((item) => item.id)},
                    DeleteEnterpriseScriptList,
                    () => {
                      actionRef.current?.clearSelected?.();
                      actionRef.current?.reload?.();
                    },
                  );
                },
              });
            }}
            danger={true}
          >
            批量删除
          </Button>
        </FooterToolbar>
      )}
      {/* 新增&修改分组 */}
      <GroupModal
        initialValues={targetScriptGroup}
        allDepartments={allDepartments}
        ref={groupModalRef}
        visible={groupModalVisible}
        setVisible={setGroupModalVisible}
        onFinish={async (values, action) => {
          if (action === 'create') {
            await HandleRequest({...values, sub_groups: []}, CreateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          } else {
            await HandleRequest({...values, sub_groups: []}, UpdateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          }
        }}
      />
      {/* 新增&修改话术 */}
      <ScriptModal
        allDepartments={allDepartments}
        initialValues={targetScript}
        ref={scriptModalRef}
        visible={scriptModalVisible}
        setVisible={setScriptModalVisible}
        setPropsGroupsTimestamp={setGroupItemsTimestamp}
        onCancel={() => {
          setGroupItemsTimestamp(Date.now)
        }}
        onFinish={async (values, action) => {
          if (action === 'create') {
            await HandleRequest({...values}, CreateEnterpriseScriptList, () => {
              setGroupItemsTimestamp(Date.now)
              actionRef?.current?.reload()
              scriptModalRef.current.close()
            })
          } else {
            const {reply_details, id, group_id, name, deleted_ids} = values
            await HandleRequest({reply_details, id, group_id, name, deleted_ids}, UpdateEnterpriseScriptList, () => {
              setGroupItemsTimestamp(Date.now)
              actionRef?.current?.reload()
              scriptModalRef.current.close()
            })
          }
        }}
      />
    </>
  )
}
Example #21
Source File: LidoWrapping.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('LidoWrapping', function () {
  let stETH: Token, wstETH: Token;
  let senderUser: SignerWithAddress, recipientUser: SignerWithAddress, admin: SignerWithAddress;
  let vault: Vault;
  let relayer: Contract, relayerLibrary: Contract;

  before('setup signer', async () => {
    [, admin, senderUser, recipientUser] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy Vault', async () => {
    vault = await Vault.create({ admin });

    const stETHContract = await deploy('MockStETH', { args: ['stETH', 'stETH', 18] });
    stETH = new Token('stETH', 'stETH', 18, stETHContract);

    const wstETHContract = await deploy('MockWstETH', { args: [stETH.address] });
    wstETH = new Token('wstETH', 'wstETH', 18, wstETHContract);
  });

  sharedBeforeEach('mint tokens to senderUser', async () => {
    await stETH.mint(senderUser, fp(100));
    await stETH.approve(vault.address, fp(100), { from: senderUser });

    await stETH.mint(senderUser, fp(2500));
    await stETH.approve(wstETH.address, fp(150), { from: senderUser });
    await wstETH.instance.connect(senderUser).wrap(fp(150));
  });

  sharedBeforeEach('set up relayer', async () => {
    // Deploy Relayer
    relayerLibrary = await deploy('MockBatchRelayerLibrary', { args: [vault.address, wstETH.address] });
    relayer = await deployedAt('BalancerRelayer', await relayerLibrary.getEntrypoint());

    // Authorize Relayer for all actions
    const relayerActionIds = await Promise.all(
      ['swap', 'batchSwap', 'joinPool', 'exitPool', 'setRelayerApproval', 'manageUserBalance'].map((action) =>
        actionId(vault.instance, action)
      )
    );
    await vault.grantPermissionsGlobally(relayerActionIds, relayer);

    // Approve relayer by sender
    await vault.setRelayerApproval(senderUser, relayer, true);
  });

  function encodeApprove(token: Token, amount: BigNumberish): string {
    return relayerLibrary.interface.encodeFunctionData('approveVault', [token.address, amount]);
  }

  function encodeWrap(
    sender: Account,
    recipient: Account,
    amount: BigNumberish,
    outputReference?: BigNumberish
  ): string {
    return relayerLibrary.interface.encodeFunctionData('wrapStETH', [
      TypesConverter.toAddress(sender),
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  function encodeUnwrap(
    sender: Account,
    recipient: Account,
    amount: BigNumberish,
    outputReference?: BigNumberish
  ): string {
    return relayerLibrary.interface.encodeFunctionData('unwrapWstETH', [
      TypesConverter.toAddress(sender),
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  function encodeStakeETH(recipient: Account, amount: BigNumberish, outputReference?: BigNumberish): string {
    return relayerLibrary.interface.encodeFunctionData('stakeETH', [
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  function encodeStakeETHAndWrap(recipient: Account, amount: BigNumberish, outputReference?: BigNumberish): string {
    return relayerLibrary.interface.encodeFunctionData('stakeETHAndWrap', [
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  describe('primitives', () => {
    const amount = fp(1);

    describe('wrapStETH', () => {
      let tokenSender: Account, tokenRecipient: Account;

      context('sender = senderUser, recipient = relayer', () => {
        beforeEach(() => {
          tokenSender = senderUser;
          tokenRecipient = relayer;
        });
        testWrap();
      });

      context('sender = senderUser, recipient = senderUser', () => {
        beforeEach(() => {
          tokenSender = senderUser;
          tokenRecipient = senderUser;
        });
        testWrap();
      });

      context('sender = relayer, recipient = relayer', () => {
        beforeEach(async () => {
          await stETH.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = relayer;
        });
        testWrap();
      });

      context('sender = relayer, recipient = senderUser', () => {
        beforeEach(async () => {
          await stETH.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = senderUser;
        });
        testWrap();
      });

      function testWrap(): void {
        it('wraps with immediate amounts', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);

          const receipt = await (
            await relayer.connect(senderUser).multicall([encodeWrap(tokenSender, tokenRecipient, amount)])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? wstETH : relayer),
              value: amount,
            },
            stETH
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? ZERO_ADDRESS : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWstETHAmount,
            },
            wstETH
          );
        });

        it('stores wrap output as chained reference', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);

          await relayer
            .connect(senderUser)
            .multicall([encodeWrap(tokenSender, tokenRecipient, amount, toChainedReference(0))]);

          await expectChainedReferenceContents(relayer, toChainedReference(0), expectedWstETHAmount);
        });

        it('wraps with chained references', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);
          await setChainedReferenceContents(relayer, toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeWrap(tokenSender, tokenRecipient, toChainedReference(0))])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? wstETH : relayer),
              value: amount,
            },
            stETH
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? ZERO_ADDRESS : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWstETHAmount,
            },
            wstETH
          );
        });
      }
    });

    describe('unwrapWstETH', () => {
      let tokenSender: Account, tokenRecipient: Account;

      context('sender = senderUser, recipient = relayer', () => {
        beforeEach(async () => {
          await wstETH.approve(vault.address, fp(10), { from: senderUser });
          tokenSender = senderUser;
          tokenRecipient = relayer;
        });
        testUnwrap();
      });

      context('sender = senderUser, recipient = senderUser', () => {
        beforeEach(async () => {
          await wstETH.approve(vault.address, fp(10), { from: senderUser });
          tokenSender = senderUser;
          tokenRecipient = senderUser;
        });
        testUnwrap();
      });

      context('sender = relayer, recipient = relayer', () => {
        beforeEach(async () => {
          await wstETH.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = relayer;
        });
        testUnwrap();
      });

      context('sender = relayer, recipient = senderUser', () => {
        beforeEach(async () => {
          await wstETH.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = senderUser;
        });
        testUnwrap();
      });

      function testUnwrap(): void {
        it('unwraps with immediate amounts', async () => {
          const receipt = await (
            await relayer.connect(senderUser).multicall([encodeUnwrap(tokenSender, tokenRecipient, amount)])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? ZERO_ADDRESS : relayer),
              value: amount,
            },
            wstETH
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? wstETH : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: await wstETH.instance.getStETHByWstETH(amount),
            },
            stETH
          );
        });

        it('stores unwrap output as chained reference', async () => {
          await relayer
            .connect(senderUser)
            .multicall([encodeUnwrap(tokenSender, tokenRecipient, amount, toChainedReference(0))]);

          const stETHAmount = await wstETH.instance.getStETHByWstETH(amount);
          await expectChainedReferenceContents(relayer, toChainedReference(0), stETHAmount);
        });

        it('unwraps with chained references', async () => {
          await setChainedReferenceContents(relayer, toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeUnwrap(tokenSender, tokenRecipient, toChainedReference(0))])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? ZERO_ADDRESS : relayer),
              value: amount,
            },
            wstETH
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? wstETH : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: await wstETH.instance.getStETHByWstETH(amount),
            },
            stETH
          );
        });
      }
    });

    describe('stakeETH', () => {
      let tokenRecipient: Account;

      context('recipient = senderUser', () => {
        beforeEach(() => {
          tokenRecipient = senderUser;
        });
        testStake();
      });

      context('recipient = relayer', () => {
        beforeEach(() => {
          tokenRecipient = relayer;
        });
        testStake();
      });

      function testStake(): void {
        it('stakes with immediate amounts', async () => {
          const receipt = await (
            await relayer.connect(senderUser).multicall([encodeStakeETH(tokenRecipient, amount)], { value: amount })
          ).wait();

          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: relayerIsRecipient ? ZERO_ADDRESS : relayer.address,
              to: relayerIsRecipient ? relayer.address : TypesConverter.toAddress(tokenRecipient),
              value: amount,
            },
            stETH
          );
        });

        it('returns excess ETH', async () => {
          const excess = fp(1.5);
          const senderBalanceBefore = await ethers.provider.getBalance(senderUser.address);

          const tx = await relayer
            .connect(senderUser)
            .multicall([encodeStakeETH(tokenRecipient, amount)], { value: amount.add(excess) });
          const receipt = await tx.wait();

          expectTransferEvent(receipt, { value: amount }, stETH);

          const txCost = tx.gasPrice.mul(receipt.gasUsed);
          expect(await ethers.provider.getBalance(senderUser.address)).to.equal(
            senderBalanceBefore.sub(txCost).sub(amount)
          );
        });

        it('stores stake output as chained reference', async () => {
          await relayer
            .connect(senderUser)
            .multicall([encodeStakeETH(tokenRecipient, amount, toChainedReference(0))], { value: amount });

          await expectChainedReferenceContents(relayer, toChainedReference(0), amount);
        });

        it('stakes with chained references', async () => {
          await setChainedReferenceContents(relayer, toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeStakeETH(tokenRecipient, toChainedReference(0))], { value: amount })
          ).wait();

          expectEvent.inIndirectReceipt(receipt, stETH.instance.interface, 'EthStaked', { amount });

          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? ZERO_ADDRESS : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: amount,
            },
            stETH
          );
        });
      }
    });

    describe('stakeETHAndWrap', () => {
      let tokenRecipient: Account;

      context('recipient = senderUser', () => {
        beforeEach(() => {
          tokenRecipient = senderUser;
        });
        testStakeAndWrap();
      });

      context('recipient = relayer', () => {
        beforeEach(() => {
          tokenRecipient = relayer;
        });
        testStakeAndWrap();
      });

      function testStakeAndWrap(): void {
        it('stakes with immediate amounts', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeStakeETHAndWrap(tokenRecipient, amount)], { value: amount })
          ).wait();

          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? ZERO_ADDRESS : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWstETHAmount,
            },
            wstETH
          );
        });

        it('stores stake output as chained reference', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);

          await relayer
            .connect(senderUser)
            .multicall([encodeStakeETHAndWrap(tokenRecipient, amount, toChainedReference(0))], { value: amount });

          await expectChainedReferenceContents(relayer, toChainedReference(0), expectedWstETHAmount);
        });

        it('stakes with chained references', async () => {
          const expectedWstETHAmount = await wstETH.instance.getWstETHByStETH(amount);

          await setChainedReferenceContents(relayer, toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeStakeETHAndWrap(tokenRecipient, toChainedReference(0))], { value: amount })
          ).wait();

          expectEvent.inIndirectReceipt(receipt, stETH.instance.interface, 'EthStaked', { amount });

          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(relayerIsRecipient ? ZERO_ADDRESS : relayer),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWstETHAmount,
            },
            wstETH
          );
        });
      }
    });
  });

  describe('complex actions', () => {
    let WETH: Token;
    let poolTokens: TokenList;
    let poolId: string;
    let pool: StablePool;

    sharedBeforeEach('deploy pool', async () => {
      WETH = await Token.deployedAt(await vault.instance.WETH());
      poolTokens = new TokenList([WETH, wstETH]).sort();

      pool = await StablePool.create({ tokens: poolTokens, vault });
      poolId = pool.poolId;

      await WETH.mint(senderUser, fp(2));
      await WETH.approve(vault, MAX_UINT256, { from: senderUser });

      // Seed liquidity in pool
      await WETH.mint(admin, fp(200));
      await WETH.approve(vault, MAX_UINT256, { from: admin });

      await stETH.mint(admin, fp(150));
      await stETH.approve(wstETH, fp(150), { from: admin });
      await wstETH.instance.connect(admin).wrap(fp(150));
      await wstETH.approve(vault, MAX_UINT256, { from: admin });

      await pool.init({ initialBalances: fp(100), from: admin });
    });

    describe('swap', () => {
      function encodeSwap(params: {
        poolId: string;
        kind: SwapKind;
        tokenIn: Token;
        tokenOut: Token;
        amount: BigNumberish;
        sender: Account;
        recipient: Account;
        outputReference?: BigNumberish;
      }): string {
        return relayerLibrary.interface.encodeFunctionData('swap', [
          {
            poolId: params.poolId,
            kind: params.kind,
            assetIn: params.tokenIn.address,
            assetOut: params.tokenOut.address,
            amount: params.amount,
            userData: '0x',
          },
          {
            sender: TypesConverter.toAddress(params.sender),
            recipient: TypesConverter.toAddress(params.recipient),
            fromInternalBalance: false,
            toInternalBalance: false,
          },
          0,
          MAX_UINT256,
          0,
          params.outputReference ?? 0,
        ]);
      }

      describe('swap using stETH as an input', () => {
        let receipt: ContractReceipt;
        const amount = fp(1);

        sharedBeforeEach('swap stETH for WETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
              encodeApprove(wstETH, MAX_UINT256),
              encodeSwap({
                poolId,
                kind: SwapKind.GivenIn,
                tokenIn: wstETH,
                tokenOut: WETH,
                amount: toChainedReference(0),
                sender: relayer,
                recipient: recipientUser,
                outputReference: 0,
              }),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId,
            tokenIn: wstETH.address,
            tokenOut: WETH.address,
          });

          expectTransferEvent(receipt, { from: vault.address, to: recipientUser.address }, WETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wstETH.balanceOf(relayer)).to.be.eq(0);
        });
      });

      describe('swap using stETH as an output', () => {
        let receipt: ContractReceipt;
        const amount = fp(1);

        sharedBeforeEach('swap WETH for stETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeSwap({
                poolId,
                kind: SwapKind.GivenIn,
                tokenIn: WETH,
                tokenOut: wstETH,
                amount,
                sender: senderUser,
                recipient: relayer,
                outputReference: toChainedReference(0),
              }),
              encodeUnwrap(relayer.address, recipientUser.address, toChainedReference(0)),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId,
            tokenIn: WETH.address,
            tokenOut: wstETH.address,
          });

          expectTransferEvent(receipt, { from: relayer.address, to: recipientUser.address }, stETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wstETH.balanceOf(relayer)).to.be.eq(0);
        });
      });
    });

    describe('batchSwap', () => {
      function encodeBatchSwap(params: {
        swaps: Array<{
          poolId: string;
          tokenIn: Token;
          tokenOut: Token;
          amount: BigNumberish;
        }>;
        sender: Account;
        recipient: Account;
        outputReferences?: Dictionary<BigNumberish>;
      }): string {
        const outputReferences = Object.entries(params.outputReferences ?? {}).map(([symbol, key]) => ({
          index: poolTokens.findIndexBySymbol(symbol),
          key,
        }));

        return relayerLibrary.interface.encodeFunctionData('batchSwap', [
          SwapKind.GivenIn,
          params.swaps.map((swap) => ({
            poolId: swap.poolId,
            assetInIndex: poolTokens.indexOf(swap.tokenIn),
            assetOutIndex: poolTokens.indexOf(swap.tokenOut),
            amount: swap.amount,
            userData: '0x',
          })),
          poolTokens.addresses,
          {
            sender: TypesConverter.toAddress(params.sender),
            recipient: TypesConverter.toAddress(params.recipient),
            fromInternalBalance: false,
            toInternalBalance: false,
          },
          new Array(poolTokens.length).fill(MAX_INT256),
          MAX_UINT256,
          0,
          outputReferences,
        ]);
      }

      describe('swap using stETH as an input', () => {
        let receipt: ContractReceipt;
        const amount = fp(1);

        sharedBeforeEach('swap stETH for WETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
              encodeApprove(wstETH, MAX_UINT256),
              encodeBatchSwap({
                swaps: [{ poolId, tokenIn: wstETH, tokenOut: WETH, amount: toChainedReference(0) }],
                sender: relayer,
                recipient: recipientUser,
              }),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId: poolId,
            tokenIn: wstETH.address,
            tokenOut: WETH.address,
          });

          expectTransferEvent(receipt, { from: vault.address, to: recipientUser.address }, WETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wstETH.balanceOf(relayer)).to.be.eq(0);
        });
      });

      describe('swap using stETH as an output', () => {
        let receipt: ContractReceipt;
        const amount = fp(1);

        sharedBeforeEach('swap WETH for stETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeBatchSwap({
                swaps: [{ poolId, tokenIn: WETH, tokenOut: wstETH, amount }],
                sender: senderUser,
                recipient: relayer,
                outputReferences: { wstETH: toChainedReference(0) },
              }),
              encodeUnwrap(relayer.address, recipientUser.address, toChainedReference(0)),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId: poolId,
            tokenIn: WETH.address,
            tokenOut: wstETH.address,
          });

          expectTransferEvent(receipt, { from: relayer.address, to: recipientUser.address }, stETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wstETH.balanceOf(relayer)).to.be.eq(0);
        });
      });
    });

    describe('joinPool', () => {
      function encodeJoin(params: {
        poolId: string;
        sender: Account;
        recipient: Account;
        assets: TokenList;
        maxAmountsIn: BigNumberish[];
        userData: string;
        outputReference?: BigNumberish;
      }): string {
        return relayerLibrary.interface.encodeFunctionData('joinPool', [
          params.poolId,
          0, // WeightedPool
          TypesConverter.toAddress(params.sender),
          TypesConverter.toAddress(params.recipient),
          {
            assets: params.assets.addresses,
            maxAmountsIn: params.maxAmountsIn,
            userData: params.userData,
            fromInternalBalance: false,
          },
          0,
          params.outputReference ?? 0,
        ]);
      }

      let receipt: ContractReceipt;
      let senderWstETHBalanceBefore: BigNumber;
      const amount = fp(1);

      sharedBeforeEach('join the pool', async () => {
        senderWstETHBalanceBefore = await wstETH.balanceOf(senderUser);
        receipt = await (
          await relayer.connect(senderUser).multicall([
            encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
            encodeApprove(wstETH, MAX_UINT256),
            encodeJoin({
              poolId,
              assets: poolTokens,
              sender: relayer,
              recipient: recipientUser,
              maxAmountsIn: poolTokens.map(() => MAX_UINT256),
              userData: WeightedPoolEncoder.joinExactTokensInForBPTOut(
                poolTokens.map((token) => (token === wstETH ? toChainedReference(0) : 0)),
                0
              ),
            }),
          ])
        ).wait();
      });

      it('joins the pool', async () => {
        expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'PoolBalanceChanged', {
          poolId,
          liquidityProvider: relayer.address,
        });

        // BPT minted to recipient
        expectTransferEvent(
          receipt,
          { from: ZERO_ADDRESS, to: recipientUser.address },
          await Token.deployedAt(pool.address)
        );
      });

      it('does not take wstETH from the user', async () => {
        const senderWstETHBalanceAfter = await wstETH.balanceOf(senderUser);
        expect(senderWstETHBalanceAfter).to.be.eq(senderWstETHBalanceBefore);
      });

      it('does not leave dust on the relayer', async () => {
        expect(await WETH.balanceOf(relayer)).to.be.eq(0);
        expect(await wstETH.balanceOf(relayer)).to.be.eq(0);
      });
    });
  });
});
Example #22
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerTagGroupList: React.FC = () => {
  const [currentItem, setCurrentItem] = useState<CustomerTagGroupItem>({});
  const [tagGroups, setTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
  const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
  const [syncLoading, setSyncLoading] = useState<boolean>(false);
  const [actionLoading, setActionLoading] = useState<boolean>(false);
  const [inputLoading, setInputLoading] = useState<boolean>(false);
  const [minOrder, setMinOrder] = useState<number>(10000);
  const [maxOrder, setMaxOrder] = useState<number>(100000);
  const [currentInputTagGroupExtID, setCurrentInputTagGroupExtID] = useState<string>();
  const [allDepartments, setAllDepartments] = useState<DepartmentOption[]>([]);
  const [allDepartmentMap, setAllDepartmentMap] = useState<Dictionary<DepartmentOption>>({});
  const queryFilterFormRef = useRef<FormInstance>();

  useEffect(() => {
    QueryDepartment({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        const departments =
          res?.data?.items?.map((item: DepartmentInterface) => {
            return {
              label: item.name,
              value: item.ext_id,
              ...item,
            };
          }) || [];
        setAllDepartments(departments);
        setAllDepartmentMap(_.keyBy<DepartmentOption>(departments, 'ext_id'));

      } else {
        message.error(res.message);
      }
    });
    queryFilterFormRef.current?.submit();
  }, []);

  // @ts-ignore
  // @ts-ignore
  return (
    <PageContainer
      fixedHeader
      header={{
        title: '客户标签管理',
      }}
      extra={[
        <Button
          key='create'
          type='primary'
          icon={<PlusOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          onClick={() => {
            setCreateModalVisible(true);
          }}
        >
          添加标签组
        </Button>,

        <Button
          key={'sync'}
          type='dashed'
          icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          loading={syncLoading}
          onClick={async () => {
            setSyncLoading(true);
            const res: CommonResp = await Sync();
            if (res.code === 0) {
              setSyncLoading(false);
              message.success('同步成功');
              queryFilterFormRef.current?.submit();
            } else {
              setSyncLoading(false);
              message.error(res.message);
            }
          }}
        >
          同步企业微信标签
        </Button>,
      ]}
    >
      <ProCard className={styles.queryFilter}>
        <QueryFilter
          formRef={queryFilterFormRef}
          onReset={() => {
            queryFilterFormRef.current?.submit();
          }}
          onFinish={async (params: any) => {
            setActionLoading(true);
            const res: CommonResp = await Query({
              ...params,
              page_size: 5000,
              sort_field: 'order',
              sort_type: 'desc',
            });
            setActionLoading(false);
            if (res.code === 0) {
              setTagGroups(res.data.items);
              if (res.data?.items[0]) {
                setMaxOrder(res.data.items[0]?.order);
              }
              if (res.data?.items.length >= 1 && res.data?.items[res.data?.items.length - 1]) {
                let min = res.data?.items[res.data?.items.length - 1];
                min = min - 1 >= 0 ? min - 1 : 0;
                setMinOrder(min);
              }
            } else {
              message.error('查询标签失败');
              setTagGroups([]);
            }
          }}
        >
          <Form.Item label='可用部门' name='ext_department_ids'>
            <DepartmentTreeSelect
              onChange={() => {
                queryFilterFormRef.current?.submit();
              }}
              options={allDepartments}
            />
          </Form.Item>

          <ProFormText width={'md'} name='name' label='搜索' placeholder='请输入关键词'/>

        </QueryFilter>
      </ProCard>

      <ProCard style={{marginTop: 12}} bodyStyle={{paddingTop: 0}} gutter={0}>
        <Spin spinning={actionLoading}>
          {(!tagGroups || tagGroups.length === 0) && <Empty style={{marginTop: 36, marginBottom: 36}}/>}
          {tagGroups && tagGroups.length > 0 && (
            <ReactSortable<any>
              handle={'.draggable-button'}
              className={styles.tagGroupList}
              list={tagGroups}
              setList={setTagGroups}
              swap={true}
              onEnd={async (e) => {
                // @ts-ignore
                const from = tagGroups[e.newIndex];
                // @ts-ignore
                const to = tagGroups[e.oldIndex];
                const res = await ExchangeOrder({id: from.id, exchange_order_id: to.id});
                if (res.code !== 0) {
                  message.error(res.message)
                }
              }}
            >
              {tagGroups.map((tagGroup) => (
                <Row className={styles.tagGroupItem} data-id={tagGroup.id} key={tagGroup.ext_id}>
                  <Col md={4} className={styles.tagName}>
                    <h4>{tagGroup.name}</h4>
                  </Col>
                  <Col md={16} className={styles.tagList}>
                    <Row>
                      可见范围:
                      {tagGroup.department_list && !tagGroup.department_list.includes(0) ? (
                        <Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
                          {tagGroup.department_list.map((id) => (
                            <div key={id}>
                            <span>
                              <FolderFilled
                                style={{
                                  color: '#47a7ff',
                                  fontSize: 20,
                                  marginRight: 6,
                                  verticalAlign: -6,
                                }}
                              />
                              {allDepartmentMap[id]?.name}
                            </span>
                            </div>
                          ))}
                        </Space>
                      ) : (
                        <span>全部员工可见</span>
                      )}
                    </Row>
                    <Row style={{marginTop: 12}}>
                      <Space direction={'horizontal'} wrap={true}>
                        <Button
                          icon={<PlusOutlined/>}
                          onClick={() => {
                            setCurrentInputTagGroupExtID(tagGroup.ext_id);
                          }}
                        >
                          添加
                        </Button>

                        {currentInputTagGroupExtID === tagGroup.ext_id && (
                          <Input
                            autoFocus={true}
                            disabled={inputLoading}
                            placeholder='逗号分隔,回车保存'
                            onBlur={() => setCurrentInputTagGroupExtID('')}
                            onPressEnter={async (e) => {
                              setInputLoading(true);
                              const res = await CreateTag({
                                names: e.currentTarget.value
                                  .replace(',', ',')
                                  .split(',')
                                  .filter((val) => val),
                                ext_tag_group_id: tagGroup.ext_id || '',
                              });
                              if (res.code === 0) {
                                setCurrentInputTagGroupExtID('');
                                tagGroup.tags?.unshift(...res.data);
                              } else {
                                message.error(res.message);
                              }
                              setInputLoading(false);
                            }}
                          />
                        )}
                        {tagGroup.tags?.map((tag) => (
                          <Tag className={styles.tagItem} key={tag.id}>
                            {tag.name}
                          </Tag>
                        ))}
                      </Space>
                    </Row>
                  </Col>
                  <Col md={4} className={styles.groupAction}>
                    <Tooltip title="拖动可实现排序" trigger={['click']}>
                      <Button
                        className={'draggable-button'}
                        icon={<DragOutlined
                          style={{cursor: 'grabbing'}}
                        />}
                        type={'text'}
                      >
                        排序
                      </Button>
                    </Tooltip>

                    <Button
                      icon={<EditOutlined/>}
                      type={'text'}
                      onClick={() => {
                        setCurrentItem(tagGroup);
                        setEditModalVisible(true);
                      }}
                    >
                      修改
                    </Button>
                    <Button
                      icon={<DeleteOutlined/>}
                      type={'text'}
                      onClick={() => {
                        Modal.confirm({
                          title: `删除标签分组`,
                          content: `是否确认删除「${tagGroup.name}」分组?`,
                          okText: '删除',
                          okType: 'danger',
                          cancelText: '取消',
                          onOk() {
                            return HandleRequest({ext_ids: [tagGroup.ext_id]}, Delete, () => {
                              queryFilterFormRef.current?.submit();
                            });
                          },
                        });
                      }}
                    >
                      删除
                    </Button>
                  </Col>
                </Row>
              ))}
            </ReactSortable>
          )}
        </Spin>
      </ProCard>

      <CreateModalForm
        // 创建标签
        type={'create'}
        minOrder={minOrder}
        maxOrder={maxOrder}
        allDepartments={allDepartments}
        setVisible={setCreateModalVisible}
        initialValues={{tags: [{name: ''}], department_list: [0]}}
        visible={createModalVisible}
        onFinish={async (values) => {
          await HandleRequest(values, Create, () => {
            queryFilterFormRef.current?.submit();
            setCreateModalVisible(false);
          });
        }}
      />

      <CreateModalForm
        // 修改标签
        type={'edit'}
        destroyOnClose={true}
        minOrder={minOrder}
        maxOrder={maxOrder}
        allDepartments={allDepartments}
        setVisible={setEditModalVisible}
        visible={editModalVisible}
        initialValues={currentItem}
        onFinish={async (values) => {
          await HandleRequest(values, Update, () => {
            queryFilterFormRef.current?.submit();
            setEditModalVisible(false);
          });
        }}
      />
    </PageContainer>
  );
}
Example #23
Source File: InternalBalance.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('Internal Balance', () => {
  let admin: SignerWithAddress, sender: SignerWithAddress, recipient: SignerWithAddress;
  let relayer: SignerWithAddress, otherRecipient: SignerWithAddress;
  let authorizer: Contract, vault: Contract;
  let tokens: TokenList, weth: Token;

  before('setup signers', async () => {
    [, admin, sender, recipient, otherRecipient, relayer] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy vault & tokens', async () => {
    tokens = await TokenList.create(['DAI', 'MKR'], { sorted: true });
    weth = await TokensDeployer.deployToken({ symbol: 'WETH' });

    authorizer = await deploy('TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] });
    vault = await deploy('Vault', { args: [authorizer.address, weth.address, MONTH, MONTH] });
  });

  describe('deposit internal balance', () => {
    const kind = OP_KIND.DEPOSIT_INTERNAL;
    const initialBalance = bn(10);

    const itHandlesDepositsProperly = (amount: BigNumber, relayed = false) => {
      it('transfers the tokens from the sender to the vault', async () => {
        await expectBalanceChange(
          () =>
            vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ]),
          tokens,
          [
            { account: sender.address, changes: { DAI: -amount } },
            { account: vault.address, changes: { DAI: amount } },
          ]
        );
      });

      it('deposits the internal balance into the recipient account', async () => {
        const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

        await vault.manageUserBalance([
          { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
        ]);

        const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0]);

        const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
        expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0].add(amount));
      });

      it('returns ETH if any is sent', async () => {
        const senderAddress = relayed ? relayer.address : sender.address;
        const previousBalance = await ethers.provider.getBalance(senderAddress);

        const gasPrice = await ethers.provider.getGasPrice();
        const receipt: ContractReceipt = await (
          await vault.manageUserBalance(
            [{ kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address }],
            { value: 100, gasPrice }
          )
        ).wait();

        const ethSpent = receipt.gasUsed.mul(gasPrice);

        const currentBalance = await ethers.provider.getBalance(senderAddress);
        expect(previousBalance.sub(currentBalance)).to.equal(ethSpent);
      });

      it('emits an event', async () => {
        const receipt = await (
          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ])
        ).wait();

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.DAI.address,
          delta: amount,
        });
      });
    };

    context('when the sender is the user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the asset is a token', () => {
        context('when the sender does hold enough balance', () => {
          sharedBeforeEach('mint tokens', async () => {
            await tokens.DAI.mint(sender, initialBalance);
          });

          context('when the given amount is approved by the sender', () => {
            sharedBeforeEach('approve tokens', async () => {
              await tokens.DAI.approve(vault, initialBalance, { from: sender });
            });

            context('when tokens and balances match', () => {
              context('when depositing zero balance', () => {
                const depositAmount = bn(0);

                itHandlesDepositsProperly(depositAmount);
              });

              context('when depositing some balance', () => {
                const depositAmount = initialBalance;

                itHandlesDepositsProperly(depositAmount);
              });
            });
          });

          context('when the given amount is not approved by the sender', () => {
            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: initialBalance,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_ALLOWANCE');
            });
          });
        });

        context('when the sender does not hold enough balance', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_BALANCE');
          });
        });
      });

      context('when the asset is ETH', () => {
        const amount = bn(100e18);

        sharedBeforeEach('mint tokens', async () => {
          await weth.mint(sender.address, amount);
          await weth.approve(vault, amount, { from: sender });
        });

        it('does not take WETH from the sender', async () => {
          await expectBalanceChange(
            () =>
              vault.manageUserBalance(
                [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
                { value: amount }
              ),
            tokens,
            { account: sender }
          );
        });

        it('increases the WETH internal balance for the recipient', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount);
        });

        it('emits an event with WETH as the token address', async () => {
          const receipt: ContractReceipt = await (
            await vault.manageUserBalance(
              [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
              { value: amount }
            )
          ).wait();

          expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
            user: recipient.address,
            token: weth.address,
            delta: amount,
          });
        });

        it('accepts deposits of both ETH and WETH', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [
              { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
              { kind, asset: weth.address, amount, sender: sender.address, recipient: recipient.address },
            ],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount.mul(2));
        });

        it('accepts multiple ETH deposits', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
            ],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount);
        });

        it('reverts if not enough ETH was supplied', async () => {
          // Send ETH to the Vault to make sure that the test fails because of the supplied ETH, even if the Vault holds
          // enough to mint the WETH using its own.
          await forceSendEth(vault, amount);

          await expect(
            vault.manageUserBalance(
              [
                {
                  kind,
                  asset: ETH_TOKEN_ADDRESS,
                  amount: amount.div(2),
                  sender: sender.address,
                  recipient: recipient.address,
                },
                {
                  kind,
                  asset: ETH_TOKEN_ADDRESS,
                  amount: amount.div(2),
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ],
              { value: amount.sub(1) }
            )
          ).to.be.revertedWith('INSUFFICIENT_ETH');
        });
      });
    });

    context('when the sender is a relayer', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens for sender', async () => {
        await tokens.DAI.mint(sender, initialBalance);
        await tokens.DAI.approve(vault, initialBalance, { from: sender });
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed to deposit by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesDepositsProperly(initialBalance, true);

          context('when the asset is ETH', () => {
            it('returns excess ETH to the relayer', async () => {
              const amount = bn(100e18);

              const relayerBalanceBefore = await ethers.provider.getBalance(relayer.address);

              const gasPrice = await ethers.provider.getGasPrice();
              const receipt: ContractReceipt = await (
                await vault.manageUserBalance(
                  [
                    {
                      kind,
                      asset: ETH_TOKEN_ADDRESS,
                      amount: amount.sub(42),
                      sender: sender.address,
                      recipient: recipient.address,
                    },
                  ],
                  { value: amount, gasPrice }
                )
              ).wait();
              const txETH = receipt.gasUsed.mul(gasPrice);

              const relayerBalanceAfter = await ethers.provider.getBalance(relayer.address);

              const ethSpent = txETH.add(amount).sub(42);
              expect(relayerBalanceBefore.sub(relayerBalanceAfter)).to.equal(ethSpent);
            });
          });
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('withdraw internal balance', () => {
    const kind = OP_KIND.WITHDRAW_INTERNAL;

    const itHandlesWithdrawalsProperly = (depositedAmount: BigNumber, amount: BigNumber) => {
      context('when tokens and balances match', () => {
        it('transfers the tokens from the vault to recipient', async () => {
          await expectBalanceChange(
            () =>
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: amount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ]),
            tokens,
            { account: recipient, changes: { DAI: amount } }
          );
        });

        it('withdraws the internal balance from the sender account', async () => {
          const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ]);

          const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
          expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0].sub(amount));

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
          expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0]);
        });

        it('emits an event', async () => {
          const receipt = await (
            await vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ])
          ).wait();

          expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
            user: sender.address,
            token: tokens.DAI.address,
            delta: amount.mul(-1),
          });
        });
      });
    };

    context('when the sender is a user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the asset is a token', () => {
        context('when the sender has enough internal balance', () => {
          const depositedAmount = bn(10e18);

          sharedBeforeEach('deposit internal balance', async () => {
            await tokens.DAI.mint(sender, depositedAmount);
            await tokens.DAI.approve(vault, depositedAmount, { from: sender });
            await vault.manageUserBalance([
              {
                kind: OP_KIND.DEPOSIT_INTERNAL,
                asset: tokens.DAI.address,
                amount: depositedAmount,
                sender: sender.address,
                recipient: sender.address,
              },
            ]);
          });

          context('when requesting all the available balance', () => {
            const amount = depositedAmount;

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('when requesting part of the balance', () => {
            const amount = depositedAmount.div(2);

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('when requesting no balance', () => {
            const amount = bn(0);

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('with requesting more balance than available', () => {
            const amount = depositedAmount.add(1);

            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: amount,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('INSUFFICIENT_INTERNAL_BALANCE');
            });
          });
        });

        context('when the sender does not have any internal balance', () => {
          const amount = 1;

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: amount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('INSUFFICIENT_INTERNAL_BALANCE');
          });
        });
      });

      context('when the asset is ETH', () => {
        const amount = bn(100e18);

        context('when the sender has enough internal balance', () => {
          sharedBeforeEach('deposit internal balance', async () => {
            await weth.mint(sender, amount, { from: sender });
            await weth.approve(vault, amount, { from: sender });
            await vault.manageUserBalance([
              {
                kind: OP_KIND.DEPOSIT_INTERNAL,
                asset: weth.address,
                amount: amount,
                sender: sender.address,
                recipient: sender.address,
              },
            ]);
          });

          it('does not send WETH to the recipient', async () => {
            await expectBalanceChange(
              () =>
                vault.manageUserBalance([
                  { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
                ]),
              tokens,
              { account: recipient }
            );
          });

          it('decreases the WETH internal balance for the sender', async () => {
            const previousSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            await vault.manageUserBalance([
              { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
            ]);

            const currentSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            expect(previousSenderBalance[0].sub(currentSenderBalance[0])).to.equal(amount);
          });

          it('emits an event with WETH as the token address', async () => {
            const receipt: ContractReceipt = await (
              await vault.manageUserBalance([
                { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
              ])
            ).wait();

            expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
              user: sender.address,
              token: weth.address,
              delta: amount.mul(-1),
            });
          });

          it('accepts withdrawals of both ETH and WETH', async () => {
            const previousSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            await vault.manageUserBalance([
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
              {
                kind,
                asset: weth.address,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
            ]);

            const currentSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            expect(previousSenderBalance[0].sub(currentSenderBalance[0])).to.equal(amount);
          });
        });
      });
    });

    context('when the sender is a relayer', () => {
      const depositedAmount = bn(10e18);

      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens and deposit to internal balance', async () => {
        await tokens.DAI.mint(sender, depositedAmount);
        await tokens.DAI.approve(vault, depositedAmount, { from: sender });
        await vault.connect(sender).manageUserBalance([
          {
            kind: OP_KIND.DEPOSIT_INTERNAL,
            asset: tokens.DAI.address,
            amount: depositedAmount,
            sender: sender.address,
            recipient: sender.address,
          },
        ]);
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesWithdrawalsProperly(depositedAmount, depositedAmount);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('transfer internal balance', () => {
    const kind = OP_KIND.TRANSFER_INTERNAL;

    function itHandlesTransfersProperly(
      initialBalances: Dictionary<BigNumber>,
      transferredAmounts: Dictionary<BigNumber>
    ) {
      const amounts = Object.values(transferredAmounts);

      it('transfers the tokens from the sender to a single recipient', async () => {
        const previousSenderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const previousRecipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);

        await vault.manageUserBalance(
          tokens.map((token, i) => ({
            kind,
            asset: token.address,
            amount: amounts[i],
            sender: sender.address,
            recipient: recipient.address,
          }))
        );

        const senderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const recipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);

        for (let i = 0; i < tokens.addresses.length; i++) {
          expect(senderBalances[i]).to.equal(previousSenderBalances[i].sub(amounts[i]));
          expect(recipientBalances[i]).to.equal(previousRecipientBalances[i].add(amounts[i]));
        }
      });

      it('transfers the tokens from the sender to multiple recipients', async () => {
        const previousSenderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const previousRecipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);
        const previousOtherRecipientBalances = await vault.getInternalBalance(otherRecipient.address, tokens.addresses);

        await vault.manageUserBalance([
          {
            kind,
            asset: tokens.first.address,
            amount: amounts[0],
            sender: sender.address,
            recipient: recipient.address,
          },
          {
            kind,
            asset: tokens.second.address,
            amount: amounts[1],
            sender: sender.address,
            recipient: otherRecipient.address,
          },
        ]);

        const senderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const recipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);
        const otherRecipientBalances = await vault.getInternalBalance(otherRecipient.address, tokens.addresses);

        for (let i = 0; i < tokens.addresses.length; i++) {
          expect(senderBalances[i]).to.equal(previousSenderBalances[i].sub(amounts[i]));
        }

        expect(recipientBalances[0]).to.equal(previousRecipientBalances[0].add(amounts[0]));
        expect(recipientBalances[1]).to.equal(previousRecipientBalances[1]);

        expect(otherRecipientBalances[0]).to.equal(previousOtherRecipientBalances[0]);
        expect(otherRecipientBalances[1]).to.equal(previousOtherRecipientBalances[1].add(amounts[1]));
      });

      it('does not affect the token balances of the sender nor the recipient', async () => {
        const previousBalances: Dictionary<Dictionary<BigNumber>> = {};

        await tokens.asyncEach(async (token: Token) => {
          const senderBalance = await token.balanceOf(sender.address);
          const recipientBalance = await token.balanceOf(recipient.address);
          previousBalances[token.symbol] = { sender: senderBalance, recipient: recipientBalance };
        });

        await vault.manageUserBalance(
          tokens.map((token, i) => ({
            kind,
            asset: token.address,
            amount: amounts[i],
            sender: sender.address,
            recipient: recipient.address,
          }))
        );

        await tokens.asyncEach(async (token: Token) => {
          const senderBalance = await token.balanceOf(sender.address);
          expect(senderBalance).to.equal(previousBalances[token.symbol].sender);

          const recipientBalance = await token.balanceOf(recipient.address);
          expect(recipientBalance).to.equal(previousBalances[token.symbol].recipient);
        });
      });

      it('emits an event for each transfer', async () => {
        const receipt = await (
          await vault.manageUserBalance(
            tokens.map((token, i) => ({
              kind,
              asset: token.address,
              amount: amounts[i],
              sender: sender.address,
              recipient: recipient.address,
            }))
          )
        ).wait();

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: sender.address,
          token: tokens.DAI.address,
          delta: transferredAmounts.DAI.mul(-1),
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: sender.address,
          token: tokens.MKR.address,
          delta: transferredAmounts.MKR.mul(-1),
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.DAI.address,
          delta: transferredAmounts.DAI,
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.MKR.address,
          delta: transferredAmounts.MKR,
        });
      });
    }

    function depositInitialBalances(initialBalances: Dictionary<BigNumber>) {
      sharedBeforeEach('deposit initial balances', async () => {
        const balances = await tokens.asyncMap(async (token: Token) => {
          const amount = initialBalances[token.symbol];
          await token.mint(sender, amount);
          await token.approve(vault, amount, { from: sender });
          return amount;
        });

        await vault.connect(sender).manageUserBalance(
          tokens.map((token, i) => ({
            kind: OP_KIND.DEPOSIT_INTERNAL,
            asset: token.address,
            amount: balances[i],
            sender: sender.address,
            recipient: sender.address,
          }))
        );
      });
    }

    context('when the sender is a user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      function itReverts(transferredAmounts: Dictionary<BigNumber>, errorReason = 'INSUFFICIENT_INTERNAL_BALANCE') {
        it('reverts', async () => {
          const amounts = Object.values(transferredAmounts);
          await expect(
            vault.manageUserBalance(
              tokens.map((token, i) => ({
                kind,
                asset: token.address,
                amount: amounts[i],
                sender: sender.address,
                recipient: recipient.address,
              }))
            )
          ).to.be.revertedWith(errorReason);
        });
      }

      context('when the sender specifies some balance', () => {
        const transferredAmounts = { DAI: bn(1e16), MKR: bn(2e16) };

        context('when the sender holds enough balance', () => {
          const initialBalances = { DAI: bn(1e18), MKR: bn(5e19) };

          depositInitialBalances(initialBalances);
          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });

        context('when the sender does not hold said balance', () => {
          context('when the sender does not hold enough balance of one token', () => {
            depositInitialBalances({ DAI: bn(10), MKR: bn(5e19) });

            itReverts(transferredAmounts);
          });

          context('when the sender does not hold enough balance of the other token', () => {
            depositInitialBalances({ DAI: bn(1e18), MKR: bn(5) });

            itReverts(transferredAmounts);
          });

          context('when the sender does not hold enough balance of both tokens', () => {
            depositInitialBalances({ DAI: bn(10), MKR: bn(5) });

            itReverts(transferredAmounts);
          });
        });
      });

      context('when the sender does not specify any balance', () => {
        const transferredAmounts = { DAI: bn(0), MKR: bn(0) };

        context('when the sender holds some balance', () => {
          const initialBalances: Dictionary<BigNumber> = { DAI: bn(1e18), MKR: bn(5e19) };

          depositInitialBalances(initialBalances);
          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });

        context('when the sender does not have any balance', () => {
          const initialBalances = { DAI: bn(0), MKR: bn(0) };

          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });
      });
    });

    context('when the sender is a relayer', () => {
      const transferredAmounts = { DAI: bn(1e16), MKR: bn(2e16) };
      const amounts = Object.values(transferredAmounts);

      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      depositInitialBalances(transferredAmounts);

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesTransfersProperly(transferredAmounts, transferredAmounts);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('transfer external balance', () => {
    const balance = bn(10);
    const kind = OP_KIND.TRANSFER_EXTERNAL;

    const itHandlesExternalTransfersProperly = (amount: BigNumber) => {
      it('transfers the tokens from the sender to the recipient, using the vault allowance of the sender', async () => {
        await expectBalanceChange(
          () =>
            vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ]),
          tokens,
          [
            { account: sender.address, changes: { DAI: -amount } },
            { account: vault.address, changes: { DAI: 0 } },
            { account: recipient.address, changes: { DAI: amount } },
          ]
        );
      });

      it('does not change the internal balances of the accounts', async () => {
        const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

        await vault.manageUserBalance([
          { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
        ]);

        const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0]);

        const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
        expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0]);
      });

      it(`${amount.gt(0) ? 'emits' : 'does not emit'} an event`, async () => {
        const receipt = await (
          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ])
        ).wait();

        expectEvent.notEmitted(receipt, 'InternalBalanceChanged');

        if (amount.gt(0)) {
          expectEvent.inReceipt(receipt, 'ExternalBalanceTransfer', {
            sender: sender.address,
            recipient: recipient.address,
            token: tokens.DAI.address,
            amount,
          });
        } else {
          expectEvent.notEmitted(receipt, 'ExternalBalanceTransfer');
        }
      });
    };

    context('when the sender is the user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the token is not the zero address', () => {
        context('when the sender does hold enough balance', () => {
          sharedBeforeEach('mint tokens', async () => {
            await tokens.DAI.mint(sender, balance);
          });

          context('when the given amount is approved by the sender', () => {
            sharedBeforeEach('approve tokens', async () => {
              await tokens.DAI.approve(vault, balance, { from: sender });
            });

            context('when tokens and balances match', () => {
              context('when depositing zero balance', () => {
                const transferAmount = bn(0);

                itHandlesExternalTransfersProperly(transferAmount);
              });

              context('when depositing some balance', () => {
                const transferAmount = balance;

                itHandlesExternalTransfersProperly(transferAmount);
              });
            });
          });

          context('when the given amount is not approved by the sender', () => {
            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: balance,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_ALLOWANCE');
            });
          });
        });

        context('when the sender does not hold enough balance', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_BALANCE');
          });
        });
      });
    });

    context('when the sender is a relayer', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens for sender', async () => {
        await tokens.DAI.mint(sender, balance);
        await tokens.DAI.approve(vault, balance, { from: sender });
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed to transfer by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesExternalTransfersProperly(balance);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('batch', () => {
    type UserBalanceOp = {
      kind: number;
      amount: number;
      asset: string;
      sender: string;
      recipient: string;
    };

    const op = (
      kind: number,
      token: Token,
      amount: number,
      from: SignerWithAddress,
      to?: SignerWithAddress
    ): UserBalanceOp => {
      return { kind, asset: token.address, amount, sender: from.address, recipient: (to || from).address };
    };

    sharedBeforeEach('mint and approve tokens', async () => {
      await tokens.mint({ to: sender, amount: bn(1000e18) });
      await tokens.approve({ from: sender, to: vault });
    });

    sharedBeforeEach('allow relayer', async () => {
      const action = await actionId(vault, 'manageUserBalance');
      await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
      await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
      await vault.connect(recipient).setRelayerApproval(recipient.address, relayer.address, true);
    });

    context('when unpaused', () => {
      context('when all the senders allowed the relayer', () => {
        context('when all ops add up', () => {
          it('succeeds', async () => {
            const ops = [
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
              op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            ];

            await vault.connect(relayer).manageUserBalance(ops);

            expect((await vault.getInternalBalance(sender.address, [tokens.DAI.address]))[0]).to.equal(0);
            expect((await vault.getInternalBalance(sender.address, [tokens.MKR.address]))[0]).to.equal(103);

            expect((await vault.getInternalBalance(recipient.address, [tokens.DAI.address]))[0]).to.equal(5);
            expect((await vault.getInternalBalance(recipient.address, [tokens.MKR.address]))[0]).to.equal(9);

            expect((await vault.getInternalBalance(otherRecipient.address, [tokens.DAI.address]))[0]).to.equal(0);
            expect((await vault.getInternalBalance(otherRecipient.address, [tokens.MKR.address]))[0]).to.equal(8);

            expect(await tokens.MKR.balanceOf(otherRecipient)).to.be.equal(200);
          });
        });

        context('when all ops do not add up', () => {
          it('reverts', async () => {
            const ops = [
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
              op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 10, recipient),
            ];

            await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith(
              'INSUFFICIENT_INTERNAL_BALANCE'
            );
          });
        });
      });

      context('when one of the senders did not allow the relayer', () => {
        it('reverts', async () => {
          const ops = [
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
            op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, otherRecipient),
          ];

          await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
        });
      });
    });

    context('when paused', () => {
      sharedBeforeEach('deposit some internal balances', async () => {
        const ops = [
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 1, sender, sender),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 50, sender, otherRecipient),
        ];

        await vault.connect(relayer).manageUserBalance(ops);

        await vault.connect(otherRecipient).setRelayerApproval(otherRecipient.address, relayer.address, true);
      });

      sharedBeforeEach('pause', async () => {
        const action = await actionId(vault, 'setPaused');
        await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]);
        await vault.connect(admin).setPaused(true);
      });

      context('when only withdrawing internal balance', () => {
        it('succeeds', async () => {
          const ops = [
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 10, otherRecipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 8, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 3, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 35, otherRecipient),
          ];

          await vault.connect(relayer).manageUserBalance(ops);

          expect((await vault.getInternalBalance(sender.address, [tokens.DAI.address]))[0]).to.equal(0);
          expect((await vault.getInternalBalance(sender.address, [tokens.MKR.address]))[0]).to.equal(0);

          expect((await vault.getInternalBalance(recipient.address, [tokens.DAI.address]))[0]).to.equal(5);
          expect((await vault.getInternalBalance(recipient.address, [tokens.MKR.address]))[0]).to.equal(9);

          expect((await vault.getInternalBalance(otherRecipient.address, [tokens.DAI.address]))[0]).to.equal(5);
          expect((await vault.getInternalBalance(otherRecipient.address, [tokens.MKR.address]))[0]).to.equal(0);
        });
      });

      context('when trying to perform multiple ops', () => {
        it('reverts', async () => {
          const ops = [
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient, recipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
            op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, otherRecipient, recipient),
          ];

          await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith('PAUSED');
        });
      });
    });
  });
});
Example #24
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerList: React.FC = () => {
  const [exportLoading, setExportLoading] = useState<boolean>(false);
  const [extraFilterParams] = useState<any>();// setExtraFilterParams
  const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
  const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>({});
  const actionRef = useRef<ActionType>();
  const queryFormRef = useRef<FormInstance>();
  const [syncLoading, setSyncLoading] = useState<boolean>(false);
  const [selectedItems, setSelectedItems] = useState<CustomerItem[]>([]);
  const [allTagGroups, setAllTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [batchTagModalVisible, setBatchTagModalVisible] = useState<boolean>(false);

  const formattedParams = (originParams: any) => {
    const params = {...originParams, ...extraFilterParams};
    if (params.created_at) {
      [params.start_time, params.end_time] = params.created_at;
      delete params.created_at;
    }

    if (params.relation_create_at) {
      [params.connection_create_start, params.connection_create_end] = params.relation_create_at;
      delete params.relation_create_at;
    }

    if (params.add_way) {
      params.channel_type = params.add_way;
      delete params.add_way;
    }

    return params;
  };

  useEffect(() => {
    QueryCustomerTagGroups({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllTagGroups(res?.data?.items);
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QuerySimpleStaffs({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        const staffs = res?.data?.items?.map((item: SimpleStaffInterface) => {
          return {
            label: item.name,
            value: item.ext_id,
            ...item,
          };
        }) || [];
        setAllStaffs(staffs);
        setStaffMap(_.keyBy<StaffOption>(staffs, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);

  const columns: ProColumns<CustomerItem>[] = [
    {
      fixed: 'left',
      title: '客户名',
      dataIndex: 'name',
      valueType: 'text',
      render: (dom, item) => {
        return (
          <div className={'customer-info-field'}>
            <a key='detail' onClick={() => {
              history.push(`/staff-admin/customer-management/customer/detail?ext_customer_id=${item.ext_customer_id}`);
            }}>
              <img
                src={item.avatar}
                className={'icon'}
                alt={item.name}
              />
            </a>
            <div className={'text-group'}>
              <p className={'text'}>
                {item.name}
              </p>
              {item.corp_name && (
                <p className={'text'} style={{color: '#eda150'}}>@{item.corp_name}</p>
              )}
              {item.type === 1 && (
                <p className={'text'} style={{color: '#5ec75d'}}>@微信</p>
              )}
            </div>
          </div>
        );
      },
    },
    {
      title: '添加人',
      dataIndex: 'ext_staff_ids',
      valueType: 'text',
      width: 200,
      renderFormItem: () => {
        return (
          <StaffTreeSelect options={allStaffs} maxTagCount={4}/>
        );
      },
      render: (dom, item) => {
        const staffs: StaffItem[] = [];
        item?.staff_relations?.forEach((staff_relation) => {
          // @ts-ignore
          const staff = staffMap[staff_relation.ext_staff_id];
          if (staff) {
            staffs.push(staff);
          }
        });
        return (
          <CollapsedStaffs limit={2} staffs={staffs}/>
        );
      },
    },
    {
      title: '标签',
      dataIndex: 'ext_tag_ids',
      valueType: 'text',
      hideInSearch: false,
      renderFormItem: () => {
        return (
          <CustomerTagSelect isEditable={false} allTagGroups={allTagGroups} maxTagCount={6}/>
        );
      },
      render: (dom, item) => {
        const tags: any[] = [];
        item.staff_relations?.forEach((relation) => {
          if (relation.ext_staff_id === localStorage.getItem('extStaffAdminID')) {
            relation.customer_staff_tags?.forEach((tag) => {
              tags.push(tag);
            });
          }
        });
        return <CollapsedTags limit={6} tags={tags}/>;
      },
    },

    {
      title: '添加时间',
      dataIndex: 'created_at',
      valueType: 'dateRange',
      sorter: true,
      filtered: true,
      render: (dom, item) => {
        if (item.staff_relations && item.staff_relations.length > 0) {
          const staff_relation = item.staff_relations[0];
          return (
            <div className={styles.staffTag}
                 dangerouslySetInnerHTML={{
                   __html: moment(staff_relation.createtime)
                     .format('YYYY-MM-DD HH:mm')
                     .split(' ')
                     .join('<br />'),
                 }}
            />
          );
        }
        return <></>;
      },

    },

    {
      title: '更新时间',
      dataIndex: ' updated_at',
      valueType: 'dateRange',
      // sorter: true,
      filtered: true,
      hideInSearch: true,
      render: (dom, item) => {
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: moment(item.updated_at)
                .format('YYYY-MM-DD HH:mm')
                .split(' ')
                .join('<br />'),
            }}
          />
        );
      },
    },

    {
      title: '添加渠道',
      dataIndex: 'add_way',
      valueType: 'select',
      valueEnum: addWayEnums,
      // width: 220,
      render: (dom, item) => {
        return <span>{item.staff_relations?.map((para) => {
          return (`${addWayEnums[para.add_way || 0]}\n`);
        })}</span>;
      },
    },

    {
      title: '性别',
      dataIndex: 'gender',
      valueType: 'select',
      hideInTable: true,
      valueEnum: {
        1: '男',
        2: '女',
        3: '未知',
        0: '不限',
      },
    },

    {
      title: '账号类型',
      dataIndex: 'type',
      valueType: 'select',
      hideInTable: true,
      valueEnum: {
        1: '微信',
        2: '企业微信',
        0: '不限',
      },
    },

    {
      title: '流失状态',
      dataIndex: 'out_flow_status',
      valueType: 'select',
      hideInTable: true,
      hideInSearch: false,
      tooltip: '员工授权后的流失客户',
      valueEnum: {
        1: '已流失',
        2: '未流失',
      },
    },

    {
      title: '操作',
      width: 120,
      valueType: 'option',
      render: (dom, item) => [
        <a key='detail' onClick={() => {
          history.push(`/staff-admin/customer-management/customer/detail?ext_customer_id=${item.ext_customer_id}`);
        }}
        >详情</a>,
      ],
    },
  ];

  return (
    <PageContainer
      fixedHeader
      header={{
        title: '客户管理',
      }}
      extra={[
        <Button
          key={'export'}
          type='dashed'
          loading={exportLoading}
          icon={<CloudDownloadOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          onClick={async () => {
            setExportLoading(true);
            try {
              const content = await ExportCustomer(
                formattedParams(queryFormRef.current?.getFieldsValue()),
              );
              const blob = new Blob([content], {
                type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
              });
              FileSaver.saveAs(blob, `客户数据列表.xlsx`);
            } catch (e) {
              console.log(e);
              message.error('导出失败');
            }
            setExportLoading(false);
          }}
        >
          导出Excel
        </Button>,


        <Button
          key={'sync'}
          type='dashed'
          icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          loading={syncLoading}
          onClick={async () => {
            setSyncLoading(true);
            const res: CommonResp = await Sync();
            if (res.code === 0) {
              setSyncLoading(false);
              message.success('同步成功');
            } else {
              setSyncLoading(false);
              message.error(res.message);
            }
          }}
        >
          同步数据
        </Button>,
      ]}
    >

      <ProTable
        rowSelection={{
          onChange: (__, items) => {
            setSelectedItems(items);
          },
        }}
        formRef={queryFormRef}
        actionRef={actionRef}
        className={'table'}
        scroll={{x: 'max-content'}}
        columns={columns}
        rowKey='id'
        pagination={{
          pageSizeOptions: ['5', '10', '20', '50', '100'],
          pageSize: 5,
        }}
        toolBarRender={false}
        bordered={false}
        tableAlertRender={false}
        params={{}}
        request={async (originParams: any, sort, filter) => {
          const res = ProTableRequestAdapter(
            formattedParams(originParams),
            sort,
            filter,
            QueryCustomer,
          );
          console.log(await res);
          return await res;
        }}
        dateFormatter='string'
      />

      {selectedItems?.length > 0 && (
        // 底部选中条目菜单栏
        <FooterToolbar>
          <span>
            已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项 &nbsp;&nbsp;
          </span>
          <Divider type='vertical'/>
          <Button
            icon={<TagOutlined/>}
            type={'dashed'}
            onClick={() => {
              setBatchTagModalVisible(true);
            }}
          >
            批量打标签
          </Button>

        </FooterToolbar>
      )}

      <CustomerTagSelectionModal
        width={'630px'}
        visible={batchTagModalVisible}
        setVisible={setBatchTagModalVisible}
        onFinish={async (selectedTags) => {
          const selectedExtTagIDs = selectedTags.map((selectedTag) => selectedTag.ext_id);
          const selectedExtCustomerIDs = selectedItems.map((customer) => customer.ext_customer_id);
          await HandleRequest({
            add_ext_tag_ids: selectedExtTagIDs,
            ext_customer_ids: selectedExtCustomerIDs,
          }, UpdateCustomerTags, () => {
            // @ts-ignore
            actionRef?.current?.reloadAndRest();
            setSelectedItems([]);
          });
        }}
        allTagGroups={allTagGroups}
        isEditable={true}
        withLogicalCondition={false}
      />

    </PageContainer>
  );
}
Example #25
Source File: detail.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerDetail: React.FC = () => {
  const queryFormRef = useRef<FormInstance>();
  const actionRef = useRef<ActionType>();
  const [customerDetail, setCustomerDetail] = useState<CustomerItem>()
  const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>({});
  const [allCustomerTagGroups, setAllCustomerTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [defaultCustomerTags, setDefaultCustomerTags] = useState<CustomerTag[]>([]);
  const [defaultInternalTagsIds, setDefaultInternalTagsIds] = useState<string []>([])
  const [personalTagModalVisible, setPersonalTagModalVisible] = useState(false)
  const [customerTagModalVisible, setCustomerTagModalVisible] = useState(false)
  const [internalTagList, setInternalTagList] = useState<InternalTags.Item[]>([])
  const [internalTagListMap, setInternalTagListMap] = useState<Dictionary<InternalTags.Item>>({});
  const [initialEvents, setInitialEvents] = useState<CustomerEvents.Item[]>([])
  const [currentTab, setCurrentTab] = useState('survey')
  const [basicInfoDisplay, setBasicInfoDisplay] = useState({} as any)// 展示哪些基本信息
  const [basicInfoValues, setBasicInfoValues] = useState({} as any) // 基本信息取值
  const [remarkValues, setRemarkValues] = useState<Remark[]>([])
  const [reloadCusDataTimesTamp, setReloadCusDataTimesTamp] = useState(Date.now)
  const [formRef] = Form.useForm()

  const params = new URLSearchParams(window.location.search);
  const currentStaff = localStorage.getItem('extStaffAdminID') as string
  const extCustomerID = params.get('ext_customer_id') || "";
  if (!extCustomerID) {
    message.error('传入参数请带上ID');
  }

  const extStaff = () => {
    const staffs: StaffItem[] = [];
    customerDetail?.staff_relations?.forEach((staff_relation) => {
      // @ts-ignore
      const staff = staffMap[staff_relation.ext_staff_id];
      if (staff) {
        staffs.push(staff);
      }
    });
    return staffs;
  }

  const getCustomerDetail = () => {
    const hide = message.loading("加载数据中");
    GetCustomerDetail(extCustomerID).then(res => {
      hide();
      if (res?.code !== 0) {
        message.error("获取客户详情失败");
        return;
      }
      setCustomerDetail(res?.data);
      const cusTags: any[] = [];
      const interTagsIds: any[] = []
      res?.data?.staff_relations?.forEach((relation: any) => {
        if (relation.ext_staff_id === currentStaff) {
          relation.customer_staff_tags?.forEach((tag: any) => {
            cusTags.push({...tag, name: tag.tag_name, ext_id: tag.ext_tag_id});
          });
          relation.internal_tags?.forEach((tagId: string) => {
            interTagsIds.push(tagId);
          })
        }

      });
      setDefaultCustomerTags(cusTags)
      setDefaultInternalTagsIds(interTagsIds)
    }).catch(() => {
      hide();
    })
  }

  const getInternalTags = () => {
    QueryInternalTags({page_size: 5000, ext_staff_id: currentStaff}).then(res => {
      if (res?.code === 0) {
        setInternalTagList(res?.data?.items)
        setInternalTagListMap(_.keyBy(res?.data?.items, 'id'))
      } else {
        message.error(res?.message)
      }
    })
  }

  const getCustomerRemark = () => { // 自定义信息id-key
    QueryCustomerRemark().then(res => {
      if (res?.code === 0) {
        console.log('QueryCustomerRemark', res.data)
      } else {
        message.error(res?.message)
      }
    })
  }

  const getBasicInfoDisplay = () => {
    GetCustomerBasicInfoDisplay().then(res => {
      if (res?.code === 0) {
        const displayData = res?.data
        delete displayData.id
        delete displayData.ext_corp_id
        delete displayData.created_at
        delete displayData.updated_at
        delete displayData.deleted_at
        setBasicInfoDisplay(displayData || {})
      } else {
        message.error(res?.message)
      }
    })
  }

  const getBasicInfoAndRemarkValues = () => {
    GetBasicInfoAndRemarkValues({
      ext_customer_id: extCustomerID,
      ext_staff_id: currentStaff,
    }).then(res => {
      if (res?.code === 0) {
        const resData = res?.data
        delete resData.id
        delete resData.ext_corp_id
        delete resData.ext_creator_id
        delete resData.ext_customer_id
        delete resData.ext_staff_id
        delete resData.created_at
        delete resData.updated_at
        delete resData.deleted_at
        delete resData.remark_values
        setBasicInfoValues(resData)
        setRemarkValues(res?.data?.remark_values || [])
      }
    })
  }

  const updateBasicInfoAndRemark = (basicInfoParams: any) => {
    UpdateBasicInfoAndRemark({
      ext_staff_id: currentStaff,
      ext_customer_id: extCustomerID,
      ...basicInfoParams
    }).then(res => {
      if (res?.code === 0) {
        message.success('客户信息更新成功')
        setReloadCusDataTimesTamp(Date.now)
      } else {
        message.error('客户信息更新失败')
      }
    })
  }

  useEffect(() => {
    getInternalTags()
    getCustomerDetail()
    getCustomerRemark()
    getBasicInfoDisplay()
    getBasicInfoAndRemarkValues()
  }, [reloadCusDataTimesTamp])

  useEffect(() => {
    QueryCustomerTagGroups({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllCustomerTagGroups(res?.data?.items);
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QuerySimpleStaffs({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        const staffs = res?.data?.items?.map((item: SimpleStaffInterface) => {
          return {
            label: item.name,
            value: item.ext_id,
            ...item,
          };
        }) || [];
        setStaffMap(_.keyBy<StaffOption>(staffs, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QueryCustomerEvents({
      ext_customer_id: extCustomerID,
      ext_staff_id: currentStaff,
      page_size: 5
    }).then(res => {
      console.log('QueryCustomerEventsQueryCustomerEvents', res)
      setInitialEvents(res?.data?.items || [])
    })
  }, [])

  useEffect(() => {
    formRef.setFieldsValue(basicInfoValues)
  }, [basicInfoValues])

  return (
    <PageContainer
      fixedHeader
      onBack={() => history.go(-1)}
      backIcon={<LeftOutlined/>}
      header={{
        title: '客户详情',
      }}
    >
      <ProCard>
        <Descriptions title="客户信息" column={1}>
          <Descriptions.Item>
            <div className={'customer-info-field'}>
              <div><img src={customerDetail?.avatar} alt={customerDetail?.name} style={{borderRadius: 5}}/></div>
              <div style={{fontSize: 16, marginLeft: 10}}>
                <p>{customerDetail?.name}</p>
                {customerDetail?.corp_name && (
                  <p style={{color: '#eda150', marginTop: 10}}>@{customerDetail?.corp_name}</p>
                )}
                {customerDetail?.type === 1 && (
                  <p style={{
                    color: '#5ec75d',
                    fontSize: '13px'
                  }}>@微信</p>
                )}
              </div>
            </div>
          </Descriptions.Item>
          <div>
            <div style={{width: 70, display: 'inline-block'}}>企业标签:</div>
            <div className={styles.tagContainer}>
              <Space direction={'horizontal'} wrap={true}>
                {
                  defaultCustomerTags?.length > 0 && defaultCustomerTags?.map((tag) =>
                    <Tag
                      key={tag?.id}
                      className={'tag-item selected-tag-item'}
                    >
                      {tag?.name}
                    </Tag>
                  )}
              </Space>
            </div>
            <Button
              key='addCusTags'
              icon={<EditOutlined/>}
              type={'link'}
              onClick={() => {
                setCustomerTagModalVisible(true);
              }}
            >
              编辑
            </Button>
          </div>

          <div>
            <div style={{width: 70, display: 'inline-block'}}>个人标签:</div>
            <div className={styles.tagContainer}>
              <Space direction={'horizontal'} wrap={true}>
                {
                  defaultInternalTagsIds?.length > 0 && defaultInternalTagsIds.map(id => internalTagListMap[id])?.map((tag) =>
                    <Tag
                      key={tag?.id}
                      className={'tag-item selected-tag-item'}
                    >
                      {tag?.name}
                      <span>
                     </span>
                    </Tag>
                  )}
              </Space>
            </div>
            <Button
              key='addInternalTags'
              icon={<EditOutlined/>}
              type={'link'}
              onClick={() => {
                setPersonalTagModalVisible(true);
              }}
            >
              编辑
            </Button>
          </div>
        </Descriptions>
      </ProCard>

      <ProCard
        tabs={{
          onChange: (activeKey: string) => setCurrentTab(activeKey),
          activeKey: currentTab
        }}
        style={{marginTop: 25}}
      >
        <ProCard.TabPane key="survey" tab="客户概况">
          <div className={styles.survey}>
            <div className={styles.cusSurveyLeft}>
              <div>
                <Descriptions title={<div><ContactsFilled/>&nbsp;&nbsp;添加客服信息</div>} layout="vertical" bordered
                              column={4}>
                  <Descriptions.Item label="所属员工">
                    <CollapsedStaffs limit={2} staffs={extStaff()}/>
                  </Descriptions.Item>

                  <Descriptions.Item label="客户来源">
                       <span>
                         {customerDetail?.staff_relations?.map((para) => {
                           return (`${addWayEnums[para.add_way || 0]}\n`);
                         })}
                       </span>
                  </Descriptions.Item>
                  <Descriptions.Item label="添加时间">
                    <Space>
                      {customerDetail?.staff_relations?.map((para) => {
                        return (
                          <div className={styles.staffTag}
                               dangerouslySetInnerHTML={{
                                 __html: moment(para.createtime)
                                   .format('YYYY-MM-DD HH:mm')
                                   .split(' ')
                                   .join('<br />'),
                               }}
                          />
                        );
                      })}
                    </Space>
                  </Descriptions.Item>
                  <Descriptions.Item label="更新时间">
                    <div
                      dangerouslySetInnerHTML={{
                        __html: moment(customerDetail?.updated_at)
                          .format('YYYY-MM-DD HH:mm')
                          .split(' ')
                          .join('<br />'),
                      }}
                    />
                  </Descriptions.Item>
                </Descriptions>
              </div>
              <Form form={formRef} onFinish={(values) => {
                console.log('ooooooooooooooovaluesvalues', values)
                const basicInfoParams = {...values}
                updateBasicInfoAndRemark(basicInfoParams)
              }}>
                <div style={{paddingTop: 20}} className={styles.baseInfoContainer}>
                  <Descriptions
                    title={<div><BookFilled/>&nbsp;&nbsp;基本信息</div>}
                    bordered
                    column={2}
                    size={'small'}
                  >
                    {
                      Object.keys(basicInfoDisplay).map(key => {
                        return <Descriptions.Item label={basicInfo[key]}>
                          <TableInput name={key} />
                        </Descriptions.Item>
                      })
                    }
                  </Descriptions>
                </div>
                {
                  remarkValues.length > 0 && <div style={{paddingTop: 20}} className={styles.customInfoContainer}>
                    <Descriptions
                      title={<div><EditFilled/>&nbsp;&nbsp;自定义信息</div>}
                      bordered
                      column={2}
                      size={'small'}
                    >
                      <Descriptions.Item label="sfdsf">
                        <TableInput name={'aa'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="违法的">
                        <TableInput name={'bb'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="sdf434">
                        <TableInput name={'cc'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="yjkyujy">
                        <TableInput name={'dd'}/>
                      </Descriptions.Item>
                    </Descriptions>
                  </div>
                }
                <div style={{display: 'flex', justifyContent: 'center', marginTop: 40}}>
                  <Space>
                    <Button onClick={() => formRef.setFieldsValue(basicInfoValues)}>重置</Button>
                    <Button type={"primary"} onClick={() => {
                      formRef.submit()
                    }}>提交</Button>
                  </Space>
                </div>
              </Form>
            </div>

            <div className={styles.cusSurveyRight}>
              <div className={styles.eventsTitle}>
                <span className={styles.titleText}><SoundFilled/>&nbsp;&nbsp;客户动态</span>
                <a onClick={() => setCurrentTab('events')} style={{fontSize: 12}}>查看更多<RightOutlined/></a>
              </div>
              <Events data={initialEvents.filter(elem => elem !== null)} simpleRender={true} staffMap={staffMap}
                      extCustomerID={extCustomerID}/>
            </div>
          </div>

        </ProCard.TabPane>

        <ProCard.TabPane key="events" tab="客户动态">
          <Events staffMap={staffMap} extCustomerID={extCustomerID}/>
        </ProCard.TabPane>

        <ProCard.TabPane key="room" tab="所在群聊">
          <ProTable<GroupChatItem>
            search={false}
            formRef={queryFormRef}
            actionRef={actionRef}
            className={'table'}
            scroll={{x: 'max-content'}}
            columns={columns}
            rowKey="id"
            toolBarRender={false}
            bordered={false}
            tableAlertRender={false}
            dateFormatter="string"
            request={async (originParams: any, sort, filter) => {
              return ProTableRequestAdapter(
                originParams,
                sort,
                filter,
                QueryCustomerGroupsList,
              );
            }}
          />
        </ProCard.TabPane>

        <ProCard.TabPane key="chat" tab="聊天记录">
          {
            setStaffMap[currentStaff]?.enable_msg_arch === 1 ? <Button
                key={'chatSession'}
                type={"link"}
                icon={<ClockCircleOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
                onClick={() => {
                  window.open(`/staff-admin/corp-risk-control/chat-session?staff=${currentStaff}`)
                }}
              >
                聊天记录查询
              </Button>
              :
              <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={<span>员工暂未开启消息存档</span>}/>
          }
        </ProCard.TabPane>

      </ProCard>

      <CustomerTagSelectionModal
        type={'customerDetailEnterpriseTag'}
        isEditable={true}
        withLogicalCondition={false}
        width={'630px'}
        visible={customerTagModalVisible}
        setVisible={setCustomerTagModalVisible}
        defaultCheckedTags={defaultCustomerTags}
        onFinish={(selectedTags) => {
          const removeAry = _.difference(defaultCustomerTags.map(dt => dt.ext_id), selectedTags.map(st => st.ext_id))
          UpdateCustomerTags({
            // @ts-ignore
            add_ext_tag_ids: selectedTags.map((tag) => tag.ext_id),
            ext_customer_ids: [extCustomerID],
            ext_staff_id: currentStaff,
            // @ts-ignore
            remove_ext_tag_ids: removeAry
          }).then(() => {
            getCustomerDetail()
          })
        }}
        allTagGroups={allCustomerTagGroups}
      />

      <InternalTagModal
        width={560}
        allTags={internalTagList}
        allTagsMap={internalTagListMap}
        setAllTags={setInternalTagList}
        visible={personalTagModalVisible}
        setVisible={setPersonalTagModalVisible}
        defaultCheckedTagsIds={defaultInternalTagsIds}
        reloadTags={getInternalTags}
        onFinish={(selectedTags) => {
          console.log('selectedTags', selectedTags)
          const removeAry = _.difference(defaultInternalTagsIds, selectedTags.map(st => st.id))
          CustomerInternalTags({
            // @ts-ignore
            add_ext_tag_ids: selectedTags.map((tag) => tag.id),
            ext_customer_id: extCustomerID,
            ext_staff_id: currentStaff,
            // @ts-ignore
            remove_ext_tag_ids: removeAry
          }).then(() => {
            getCustomerDetail()
          })
        }
        }
      />
    </PageContainer>
  );
}
Example #26
Source File: PropertyPicker.tsx    From gio-design with Apache License 2.0 4 votes vote down vote up
PropertyPicker: React.FC<PropertyPickerProps> = (props: PropertyPickerProps) => {
  const {
    value: initialValue,
    searchBar,
    loading = false,
    dataSource: originDataSource,
    recentlyStorePrefix = '_gio',
    onChange,
    onSelect,
    onClick,
    detailVisibleDelay = 600,
    fetchDetailData = (data: PropertyItem): Promise<PropertyInfo> => Promise.resolve({ ...data }),
    disabledValues = [],
    shouldUpdateRecentlyUsed = true,
    className,
    ...rest
  } = props;
  const locale = useLocale('PropertyPicker');
  const localeText = { ...defaultLocale, ...locale } as typeof defaultLocale;
  const Tabs = toPairs(PropertyTypes(localeText)).map((v) => ({ key: v[0], children: v[1] }));
  const [scope, setScope] = useState('all');
  const [keyword, setKeyword] = useState<string | undefined>('');
  const [recentlyUsedInMemo, setRecentlyUsedInMemo] = useState<{
    [key: string]: any[];
  }>();
  const [recentlyUsed, setRecentlyUsed] = useLocalStorage<{
    [key: string]: any[];
  }>(`${recentlyStorePrefix}_propertyPicker`, {
    all: [],
  });

  useEffect(() => {
    if (shouldUpdateRecentlyUsed) {
      setRecentlyUsedInMemo(recentlyUsed);
    }
  }, [recentlyUsed, shouldUpdateRecentlyUsed]);

  const [currentValue, setCurrentValue] = useState<PropertyValue | undefined>(initialValue);

  const prefixCls = usePrefixCls('property-picker-legacy');

  const [detailVisible, setDetailVisible] = useState(false);
  const debounceSetDetailVisible = useDebounceFn((visible: boolean) => {
    setDetailVisible(visible);
  }, detailVisibleDelay);
  const [dataList, setDataList] = useState<PropertyItem[]>([]);
  const navRef = useRef([{ key: 'all', children: localeText.allText }]);
  useEffect(() => {
    // 如果是Dimension类型 需要做一个数据转换
    let propertiItemList: PropertyItem[] = [];
    if (originDataSource && originDataSource.length) {
      if (!('value' in originDataSource[0])) {
        propertiItemList = originDataSource.map((v) => {
          const item = dimensionToPropertyItem(v as Dimension, localeText);
          item.itemIcon = () => <IconRender group={item.iconId} />;
          return item;
        });
      } else {
        propertiItemList = originDataSource.map((v) => {
          const item = v as PropertyItem;
          item.itemIcon = () => <IconRender group={item.iconId} />;
          return item;
        });
      }
    }
    const list = propertiItemList.map((v) => {
      const disabled = !!disabledValues && disabledValues.includes(v.id);

      return {
        ...v,
        disabled,
        pinyinName: getShortPinyin(v.label ?? ''),
      };
    });

    setDataList(list);
    /**
     * 设置属性类型tab,如果传入的列表没有对应的类型 不显示该tab
     */
    const types = uniq(propertiItemList.map((p) => p.type));
    const tabs = Tabs.filter((t) => types.indexOf(t.key) > -1);
    navRef.current = [{ key: 'all', children: localeText.allText }].concat(tabs);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [localeText.allText, originDataSource]);

  /**
   * 搜索关键字的方法,支持拼音匹配
   * @param input 带匹配的项
   * @param key 匹配的关键字
   */
  const keywordFilter = (input = '', key = '') => {
    if (!input || !key) return true;
    return !!pinyinMatch?.match(input, key);
  };

  /**
   * 属性列表数据源
   */
  const dataSource = useMemo(() => {
    const filteredData = dataList.filter((item) => {
      const { label, type, groupId, valueType } = item;
      if (groupId === 'virtual' && valueType !== 'string') {
        return false;
      }
      if (scope === 'all') {
        return keywordFilter(label, keyword);
      }
      return type === scope && keywordFilter(label, keyword);
    });

    // 按照分组排序
    const sortedData = orderBy(filteredData, ['typeOrder', 'groupOrder', 'pinyinName']);

    // mixin 最近使用
    const rids: string[] = recentlyUsedInMemo ? recentlyUsedInMemo[scope] : [];
    const recent: PropertyItem[] = [];
    rids?.forEach((v: string) => {
      const r = filteredData.find((d) => d.value === v);
      if (r) {
        recent.push({
          ...r,
          itemIcon: () => <IconRender group={r.iconId} />,
          _groupKey: 'recently',
        });
      }
    });
    return [recent, sortedData];
  }, [dataList, keyword, recentlyUsedInMemo, scope]);

  function onTabNavChange(key: string) {
    setScope(key);
  }

  /**
   * 点选时 设置最近使用
   * @param item
   */
  function _saveRecentlyByScope(item: PropertyItem) {
    const { value: v, type } = item;
    const recent = cloneDeep(recentlyUsed);
    // save by type/scope
    const realScope = type || 'all';
    let scopedRecent = recent[realScope];
    if (!scopedRecent) {
      scopedRecent = [];
    }
    let newScopedRecent = uniq([v, ...scopedRecent]);
    if (newScopedRecent.length > 5) {
      newScopedRecent = newScopedRecent.slice(0, 5);
    }
    const allScopedRecent = recent.all || [];

    let newAllScopedRecent = uniq([v, ...allScopedRecent]);
    if (newAllScopedRecent.length > 5) {
      newAllScopedRecent = newAllScopedRecent.slice(0, 5);
    }
    recent[realScope] = newScopedRecent;
    recent.all = newAllScopedRecent;
    setRecentlyUsed(recent);
  }
  function handleSelect(node: PropertyItem) {
    setCurrentValue(node as PropertyValue);

    _saveRecentlyByScope(node);
    if (isEmpty(currentValue) || !isEqualWith(currentValue, node, (a, b) => a.value === b.value)) {
      onChange?.(node);
    }
    onSelect?.(node);
  }
  const handleSearch = (query: string) => {
    setKeyword(query);
  };
  const handleItemClick = (e: React.MouseEvent<HTMLElement>, node: PropertyItem) => {
    handleSelect(node);
    onClick?.(e);
  };
  const [recentlyPropertyItems, propertyItems] = dataSource;
  const groupDatasource = useMemo(
    () => groupBy([...propertyItems], (o) => replace(o.type, /^recently¥/, '')),
    [propertyItems]
  );

  function labelRender(item: PropertyItem) {
    const isShowIndent = Boolean(item.associatedKey && item._groupKey !== 'recently');
    return (
      <>
        <span className={classNames('item-icon', { indent: isShowIndent })}>{item.itemIcon?.()}</span>
        <span>{item.label}</span>
      </>
    );
  }
  const [hoverdNodeValue, setHoveredNodeValue] = useState<PropertyItem | undefined>();
  function getListItems(items: PropertyItem[], keyPrefix = '') {
    const handleItemMouseEnter = (data: PropertyItem) => {
      setHoveredNodeValue(data);
      debounceSetDetailVisible(true);
    };
    const handleItemMouseLeave = () => {
      setHoveredNodeValue(undefined);
      debounceSetDetailVisible.cancel();
      setDetailVisible(false);
    };
    const listItems = items.map((data: PropertyItem) => {
      const select =
        !isEmpty(currentValue) &&
        isEqualWith(currentValue, data, (a, b) => a?.value === b?.value) &&
        data._groupKey !== 'recently';
      const itemProp: ListItemProps = {
        disabled: data.disabled,
        ellipsis: true,
        key: ['item', keyPrefix, data.type, data.groupId, data.id].join('-'),
        className: classNames({ selected: select }),
        children: labelRender(data),
        onClick: (e) => handleItemClick(e, data),
        onMouseEnter: () => {
          handleItemMouseEnter(data);
        },
        onMouseLeave: () => {
          handleItemMouseLeave();
        },
      };
      return itemProp;
    });
    return listItems;
  }
  function subGroupRender(groupData: Dictionary<PropertyItem[]>) {
    const dom = keys(groupData).map((gkey) => {
      const { groupName, type } = groupData[gkey][0];
      const listItems = getListItems(groupData[gkey]);
      return (
        <ExpandableGroupOrSubGroup
          key={['exp', type, gkey].join('-')}
          groupKey={[type, gkey].join('-')}
          title={groupName}
          type="subgroup"
          items={listItems}
        />
      );
    });
    return dom as React.ReactNode;
  }
  const renderItems = () => {
    if (propertyItems?.length === 0) {
      return <Result type="empty-result" size="small" />;
    }
    const recentlyNodes = recentlyPropertyItems?.length > 0 && (
      <React.Fragment key="recentlyNodes">
        <ExpandableGroupOrSubGroup
          groupKey="recently"
          key="exp-group-recently"
          title={localeText.recent}
          type="group"
          items={getListItems(recentlyPropertyItems, 'recently')}
        />
        <List.Divider key="divider-group-recently" />
      </React.Fragment>
    );

    const groupFn = (item: PropertyItem, existIsSystem: boolean) => {
      if (existIsSystem) {
        if (item.groupId === 'tag') {
          return 'tag';
        }
        if (item.groupId === 'virtual') {
          return 'virtual';
        }
        return item.isSystem;
      }
      return item.groupId;
    };

    const groupDataNodes = keys(groupDatasource).map((key, index) => {
      const groupData = groupDatasource[key];
      const existIsSystem = has(groupData, '[0].isSystem');

      let subGroupDic;
      if (key === 'event' && 'associatedKey' in groupData[0] && groupData.length > 1) {
        subGroupDic = groupBy(
          groupData
            .filter((ele) => !ele.associatedKey)
            .map((ele) => [ele])
            ?.reduce((acc, cur) => {
              cur.push(
                ...groupData
                  .filter((e) => {
                    if (e.associatedKey === cur[0].id) {
                      if (existIsSystem) {
                        e.isSystem = cur[0].isSystem;
                      }
                      return true;
                    }
                    return false;
                  })
                  .map((item) => {
                    const { groupId, groupName } = [...cur].shift() || {};
                    return { ...item, groupId, groupName };
                  })
              );
              acc.push(...cur);
              return acc;
            }, []),
          (item) => groupFn(item, existIsSystem)
        );
      } else {
        subGroupDic = groupBy(groupData, (item) => groupFn(item, existIsSystem));
      }

      const { typeName } = groupData[0];
      // 此处的处理是 如果2级分组只有一组 提升为一级分组;如果没有这个需求删除该if分支 ;
      if (keys(subGroupDic).length === 1) {
        const items = getListItems(subGroupDic[keys(subGroupDic)[0]]);
        return (
          <React.Fragment key={`groupDataNodes-${index}`}>
            {index > 0 && <List.Divider key={`divider-group-${key}-${index}`} />}
            <ExpandableGroupOrSubGroup
              key={`exp-group-${key}`}
              groupKey={`${key}`}
              title={typeName}
              type="group"
              items={items}
            />
          </React.Fragment>
        );
      }
      return (
        <React.Fragment key={`groupDataNodes-${index}`}>
          {index > 0 && <List.Divider key={`divider-group-${key}-${index}`} />}
          <List.ItemGroup key={`group-${key}`} title={typeName} expandable={false}>
            {subGroupRender(subGroupDic)}
          </List.ItemGroup>
        </React.Fragment>
      );
    });
    const childrens = [recentlyNodes, groupDataNodes];
    return childrens as React.ReactNode;
  };
  const renderDetail = () =>
    hoverdNodeValue && <PropertyCard nodeData={hoverdNodeValue} fetchData={promisify(fetchDetailData)} />;
  return (
    <>
      <BasePicker
        {...rest}
        className={classNames(prefixCls, className)}
        renderItems={renderItems}
        detailVisible={detailVisible && !!hoverdNodeValue}
        renderDetail={renderDetail}
        loading={loading}
        searchBar={{
          placeholder: searchBar?.placeholder || localeText.searchPlaceholder,
          onSearch: handleSearch,
        }}
        tabNav={{
          items: navRef.current,
          onChange: onTabNavChange,
        }}
      />
    </>
  );
}
Example #27
Source File: UnbuttonWrapping.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('UnbuttonWrapping', function () {
  let ampl: Token, wampl: Token;
  let senderUser: SignerWithAddress, recipientUser: SignerWithAddress, admin: SignerWithAddress;
  let vault: Vault;
  let relayer: Contract, relayerLibrary: Contract;

  before('setup signer', async () => {
    [, admin, senderUser, recipientUser] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy Vault', async () => {
    vault = await Vault.create({ admin });

    const amplContract = await deploy('TestToken', {
      args: ['Mock Ampleforth', 'AMPL', 9],
    });
    ampl = new Token('Mock Ampleforth', 'AMPL', 9, amplContract);

    const wamplContract = await deploy('v2-pool-linear/MockUnbuttonERC20', {
      args: [ampl.address, 'Mock Wrapped Ampleforth', 'wAMPL'],
    });
    wampl = new Token('wampl', 'wampl', 18, wamplContract);

    await ampl.mint(admin, '1000', { from: admin });
    await ampl.instance.connect(admin).approve(wampl.address, '1000');
    await wampl.instance.connect(admin).initialize('1000000');
  });

  sharedBeforeEach('mint tokens to senderUser', async () => {
    await ampl.mint(senderUser, amplFP(100), { from: admin });
    await ampl.approve(vault.address, amplFP(100), { from: senderUser });

    await ampl.mint(senderUser, amplFP(2500), { from: admin });
    await ampl.approve(wampl.address, amplFP(150), { from: senderUser });

    await wampl.instance.connect(senderUser).deposit(amplFP(150));
  });

  sharedBeforeEach('set up relayer', async () => {
    // Deploy Relayer
    relayerLibrary = await deploy('MockBatchRelayerLibrary', { args: [vault.address, ZERO_ADDRESS] });
    relayer = await deployedAt('BalancerRelayer', await relayerLibrary.getEntrypoint());

    // Authorize Relayer for all actions
    const relayerActionIds = await Promise.all(
      ['swap', 'batchSwap', 'joinPool', 'exitPool', 'setRelayerApproval', 'manageUserBalance'].map((action) =>
        actionId(vault.instance, action)
      )
    );
    const authorizer = await deployedAt('v2-vault/TimelockAuthorizer', await vault.instance.getAuthorizer());
    const wheres = relayerActionIds.map(() => ANY_ADDRESS);
    await authorizer.connect(admin).grantPermissions(relayerActionIds, relayer.address, wheres);

    // Approve relayer by sender
    await vault.instance.connect(senderUser).setRelayerApproval(senderUser.address, relayer.address, true);
  });

  const CHAINED_REFERENCE_PREFIX = 'ba10';
  function toChainedReference(key: BigNumberish): BigNumber {
    // The full padded prefix is 66 characters long,
    // with 64 hex characters and the 0x prefix.
    const paddedPrefix = `0x${CHAINED_REFERENCE_PREFIX}${'0'.repeat(64 - CHAINED_REFERENCE_PREFIX.length)}`;
    return BigNumber.from(paddedPrefix).add(key);
  }

  function encodeApprove(token: Token, amount: BigNumberish): string {
    return relayerLibrary.interface.encodeFunctionData('approveVault', [token.address, amount]);
  }

  function encodeWrap(
    sender: Account,
    recipient: Account,
    amount: BigNumberish,
    outputReference?: BigNumberish
  ): string {
    return relayerLibrary.interface.encodeFunctionData('wrapUnbuttonToken', [
      wampl.address,
      TypesConverter.toAddress(sender),
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  function encodeUnwrap(
    sender: Account,
    recipient: Account,
    amount: BigNumberish,
    outputReference?: BigNumberish
  ): string {
    return relayerLibrary.interface.encodeFunctionData('unwrapUnbuttonToken', [
      wampl.address,
      TypesConverter.toAddress(sender),
      TypesConverter.toAddress(recipient),
      amount,
      outputReference ?? 0,
    ]);
  }

  async function setChainedReferenceContents(ref: BigNumberish, value: BigNumberish): Promise<void> {
    await relayer.multicall([relayerLibrary.interface.encodeFunctionData('setChainedReferenceValue', [ref, value])]);
  }

  async function expectChainedReferenceContents(ref: BigNumberish, expectedValue: BigNumberish): Promise<void> {
    const receipt = await (
      await relayer.multicall([relayerLibrary.interface.encodeFunctionData('getChainedReferenceValue', [ref])])
    ).wait();

    expectEvent.inIndirectReceipt(receipt, relayerLibrary.interface, 'ChainedReferenceValueRead', {
      value: bn(expectedValue),
    });
  }

  function expectTransferEvent(
    receipt: ContractReceipt,
    args: { from?: string; to?: string; value?: BigNumberish },
    token: Token
  ) {
    return expectEvent.inIndirectReceipt(receipt, token.instance.interface, 'Transfer', args, token.address);
  }

  describe('primitives', () => {
    const amount = amplFP(1);

    describe('wrap AMPL', () => {
      let tokenSender: Account, tokenRecipient: Account;

      context('sender = senderUser, recipient = relayer', () => {
        beforeEach(async () => {
          tokenSender = senderUser;
          tokenRecipient = relayer;
        });
        testWrap();
      });

      context('sender = senderUser, recipient = senderUser', () => {
        beforeEach(() => {
          tokenSender = senderUser;
          tokenRecipient = senderUser;
        });
        testWrap();
      });

      context('sender = relayer, recipient = relayer', () => {
        beforeEach(async () => {
          await ampl.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = relayer;
        });
        testWrap();
      });

      context('sender = relayer, recipient = senderUser', () => {
        beforeEach(async () => {
          await ampl.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = senderUser;
        });
        testWrap();
      });

      function testWrap(): void {
        it('wraps with immediate amounts', async () => {
          const expectedWamplAmount = await wampl.instance.underlyingToWrapper(amount);

          const receipt = await (
            await relayer.connect(senderUser).multicall([encodeWrap(tokenSender, tokenRecipient, amount)])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? wampl : relayer),
              value: amount,
            },
            ampl
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(ZERO_ADDRESS),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWamplAmount,
            },
            wampl
          );
        });

        it('stores wrap output as chained reference', async () => {
          const expectedWamplAmount = await wampl.instance.underlyingToWrapper(amount);

          await relayer
            .connect(senderUser)
            .multicall([encodeWrap(tokenSender, tokenRecipient, amount, toChainedReference(0))]);

          await expectChainedReferenceContents(toChainedReference(0), expectedWamplAmount);
        });

        it('wraps with chained references', async () => {
          const expectedWamplAmount = await wampl.instance.underlyingToWrapper(amount);
          await setChainedReferenceContents(toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeWrap(tokenSender, tokenRecipient, toChainedReference(0))])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? wampl : relayer),
              value: amount,
            },
            ampl
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(ZERO_ADDRESS),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: expectedWamplAmount,
            },
            wampl
          );
        });
      }
    });

    describe('unwrap WAMPL', () => {
      let tokenSender: Account, tokenRecipient: Account;

      context('sender = senderUser, recipient = relayer', () => {
        beforeEach(async () => {
          await wampl.approve(vault.address, fp(10), { from: senderUser });
          tokenSender = senderUser;
          tokenRecipient = relayer;
        });
        testUnwrap();
      });

      context('sender = senderUser, recipient = senderUser', () => {
        beforeEach(async () => {
          await wampl.approve(vault.address, fp(10), { from: senderUser });
          tokenSender = senderUser;
          tokenRecipient = senderUser;
        });
        testUnwrap();
      });

      context('sender = relayer, recipient = relayer', () => {
        beforeEach(async () => {
          await wampl.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = relayer;
        });
        testUnwrap();
      });

      context('sender = relayer, recipient = senderUser', () => {
        beforeEach(async () => {
          await wampl.transfer(relayer, amount, { from: senderUser });
          tokenSender = relayer;
          tokenRecipient = senderUser;
        });
        testUnwrap();
      });

      function testUnwrap(): void {
        it('unwraps with immediate amounts', async () => {
          const receipt = await (
            await relayer.connect(senderUser).multicall([encodeUnwrap(tokenSender, tokenRecipient, amount)])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? ZERO_ADDRESS : relayer),
              value: amount,
            },
            wampl
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(wampl),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: await wampl.instance.wrapperToUnderlying(amount),
            },
            ampl
          );
        });

        it('stores unwrap output as chained reference', async () => {
          await relayer
            .connect(senderUser)
            .multicall([encodeUnwrap(tokenSender, tokenRecipient, amount, toChainedReference(0))]);

          const amplAmount = await wampl.instance.wrapperToUnderlying(amount);
          await expectChainedReferenceContents(toChainedReference(0), amplAmount);
        });

        it('unwraps with chained references', async () => {
          await setChainedReferenceContents(toChainedReference(0), amount);

          const receipt = await (
            await relayer
              .connect(senderUser)
              .multicall([encodeUnwrap(tokenSender, tokenRecipient, toChainedReference(0))])
          ).wait();

          const relayerIsSender = TypesConverter.toAddress(tokenSender) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(tokenSender),
              to: TypesConverter.toAddress(relayerIsSender ? ZERO_ADDRESS : relayer),
              value: amount,
            },
            wampl
          );
          const relayerIsRecipient = TypesConverter.toAddress(tokenRecipient) === relayer.address;
          expectTransferEvent(
            receipt,
            {
              from: TypesConverter.toAddress(wampl),
              to: TypesConverter.toAddress(relayerIsRecipient ? relayer : tokenRecipient),
              value: await wampl.instance.wrapperToUnderlying(amount),
            },
            ampl
          );
        });
      }
    });
  });

  describe('complex actions', () => {
    let WETH: Token;
    let poolTokens: TokenList;
    let poolId: string;
    let pool: StablePool;

    sharedBeforeEach('deploy pool', async () => {
      WETH = await Token.deployedAt(await vault.instance.WETH());
      poolTokens = new TokenList([WETH, wampl]).sort();

      pool = await StablePool.create({ tokens: poolTokens, vault });
      poolId = pool.poolId;

      await WETH.mint(senderUser, fp(2));
      await WETH.approve(vault, MAX_UINT256, { from: senderUser });
      await WETH.mint(admin, fp(20));

      await ampl.mint(admin, amplFP(6000), { from: admin });
      await ampl.approve(wampl, amplFP(6000), { from: admin });
      await wampl.instance.connect(admin).mint(fp(6));

      await WETH.approve(vault, MAX_UINT256, { from: admin });
      await wampl.approve(vault, MAX_UINT256, { from: admin });

      // Seed liquidity in pool
      await pool.init({ initialBalances: [fp(2), fp(6)], from: admin });
    });

    describe('swap', () => {
      function encodeSwap(params: {
        poolId: string;
        kind: SwapKind;
        tokenIn: Token;
        tokenOut: Token;
        amount: BigNumberish;
        sender: Account;
        recipient: Account;
        outputReference?: BigNumberish;
      }): string {
        return relayerLibrary.interface.encodeFunctionData('swap', [
          {
            poolId: params.poolId,
            kind: params.kind,
            assetIn: params.tokenIn.address,
            assetOut: params.tokenOut.address,
            amount: params.amount,
            userData: '0x',
          },
          {
            sender: TypesConverter.toAddress(params.sender),
            recipient: TypesConverter.toAddress(params.recipient),
            fromInternalBalance: false,
            toInternalBalance: false,
          },
          0,
          MAX_UINT256,
          0,
          params.outputReference ?? 0,
        ]);
      }

      describe('swap using ampl as an input', () => {
        let receipt: ContractReceipt;
        const amount = amplFP(1);

        sharedBeforeEach('swap ampl for WETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
              encodeApprove(wampl, MAX_UINT256),
              encodeSwap({
                poolId,
                kind: SwapKind.GivenIn,
                tokenIn: wampl,
                tokenOut: WETH,
                amount: toChainedReference(0),
                sender: relayer,
                recipient: recipientUser,
                outputReference: 0,
              }),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId,
            tokenIn: wampl.address,
            tokenOut: WETH.address,
          });

          expectTransferEvent(receipt, { from: vault.address, to: recipientUser.address }, WETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wampl.balanceOf(relayer)).to.be.eq(0);
        });
      });

      describe('swap using ampl as an output', () => {
        let receipt: ContractReceipt;
        const amount = amplFP(1);

        sharedBeforeEach('swap WETH for ampl', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeSwap({
                poolId,
                kind: SwapKind.GivenIn,
                tokenIn: WETH,
                tokenOut: wampl,
                amount,
                sender: senderUser,
                recipient: relayer,
                outputReference: toChainedReference(0),
              }),
              encodeUnwrap(relayer.address, recipientUser.address, toChainedReference(0)),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId,
            tokenIn: WETH.address,
            tokenOut: wampl.address,
          });

          expectTransferEvent(receipt, { from: wampl.address, to: recipientUser.address }, ampl);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wampl.balanceOf(relayer)).to.be.eq(0);
        });
      });
    });

    describe('batchSwap', () => {
      function encodeBatchSwap(params: {
        swaps: Array<{
          poolId: string;
          tokenIn: Token;
          tokenOut: Token;
          amount: BigNumberish;
        }>;
        sender: Account;
        recipient: Account;
        outputReferences?: Dictionary<BigNumberish>;
      }): string {
        const outputReferences = Object.entries(params.outputReferences ?? {}).map(([symbol, key]) => ({
          index: poolTokens.findIndexBySymbol(symbol),
          key,
        }));

        return relayerLibrary.interface.encodeFunctionData('batchSwap', [
          SwapKind.GivenIn,
          params.swaps.map((swap) => ({
            poolId: swap.poolId,
            assetInIndex: poolTokens.indexOf(swap.tokenIn),
            assetOutIndex: poolTokens.indexOf(swap.tokenOut),
            amount: swap.amount,
            userData: '0x',
          })),
          poolTokens.addresses,
          {
            sender: TypesConverter.toAddress(params.sender),
            recipient: TypesConverter.toAddress(params.recipient),
            fromInternalBalance: false,
            toInternalBalance: false,
          },
          new Array(poolTokens.length).fill(MAX_INT256),
          MAX_UINT256,
          0,
          outputReferences,
        ]);
      }

      describe('swap using ampl as an input', () => {
        let receipt: ContractReceipt;
        const amount = amplFP(1);

        sharedBeforeEach('swap ampl for WETH', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
              encodeApprove(wampl, MAX_UINT256),
              encodeBatchSwap({
                swaps: [{ poolId, tokenIn: wampl, tokenOut: WETH, amount: toChainedReference(0) }],
                sender: relayer,
                recipient: recipientUser,
              }),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId: poolId,
            tokenIn: wampl.address,
            tokenOut: WETH.address,
          });

          expectTransferEvent(receipt, { from: vault.address, to: recipientUser.address }, WETH);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wampl.balanceOf(relayer)).to.be.eq(0);
        });
      });

      describe('swap using ampl as an output', () => {
        let receipt: ContractReceipt;
        const amount = amplFP(1);

        sharedBeforeEach('swap WETH for ampl', async () => {
          receipt = await (
            await relayer.connect(senderUser).multicall([
              encodeBatchSwap({
                swaps: [{ poolId, tokenIn: WETH, tokenOut: wampl, amount }],
                sender: senderUser,
                recipient: relayer,
                outputReferences: { wampl: toChainedReference(0) },
              }),
              encodeUnwrap(relayer.address, recipientUser.address, toChainedReference(0)),
            ])
          ).wait();
        });

        it('performs the given swap', async () => {
          expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'Swap', {
            poolId: poolId,
            tokenIn: WETH.address,
            tokenOut: wampl.address,
          });

          expectTransferEvent(receipt, { from: wampl.address, to: recipientUser.address }, ampl);
        });

        it('does not leave dust on the relayer', async () => {
          expect(await WETH.balanceOf(relayer)).to.be.eq(0);
          expect(await wampl.balanceOf(relayer)).to.be.eq(0);
        });
      });
    });

    describe('joinPool', () => {
      function encodeJoin(params: {
        poolId: string;
        sender: Account;
        recipient: Account;
        assets: TokenList;
        maxAmountsIn: BigNumberish[];
        userData: string;
        outputReference?: BigNumberish;
      }): string {
        return relayerLibrary.interface.encodeFunctionData('joinPool', [
          params.poolId,
          0, // WeightedPool
          TypesConverter.toAddress(params.sender),
          TypesConverter.toAddress(params.recipient),
          {
            assets: params.assets.addresses,
            maxAmountsIn: params.maxAmountsIn,
            userData: params.userData,
            fromInternalBalance: false,
          },
          0,
          params.outputReference ?? 0,
        ]);
      }

      let receipt: ContractReceipt;
      let senderWamplBalanceBefore: BigNumber;
      const amount = amplFP(1);

      sharedBeforeEach('join the pool', async () => {
        senderWamplBalanceBefore = await wampl.balanceOf(senderUser);
        receipt = await (
          await relayer.connect(senderUser).multicall([
            encodeWrap(senderUser.address, relayer.address, amount, toChainedReference(0)),
            encodeApprove(wampl, MAX_UINT256),
            encodeJoin({
              poolId,
              assets: poolTokens,
              sender: relayer,
              recipient: recipientUser,
              maxAmountsIn: poolTokens.map(() => MAX_UINT256),
              userData: WeightedPoolEncoder.joinExactTokensInForBPTOut(
                poolTokens.map((token) => (token === wampl ? toChainedReference(0) : 0)),
                0
              ),
            }),
          ])
        ).wait();
      });

      it('joins the pool', async () => {
        expectEvent.inIndirectReceipt(receipt, vault.instance.interface, 'PoolBalanceChanged', {
          poolId,
          liquidityProvider: relayer.address,
        });

        // BPT minted to recipient
        expectTransferEvent(
          receipt,
          { from: ZERO_ADDRESS, to: recipientUser.address },
          await Token.deployedAt(pool.address)
        );
      });

      it('does not take wampl from the user', async () => {
        const senderWamplBalanceAfter = await wampl.balanceOf(senderUser);
        expect(senderWamplBalanceAfter).to.be.eq(senderWamplBalanceBefore);
      });

      it('does not leave dust on the relayer', async () => {
        expect(await WETH.balanceOf(relayer)).to.be.eq(0);
        expect(await wampl.balanceOf(relayer)).to.be.eq(0);
      });
    });
  });
});
Example #28
Source File: api-view.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiView = ({ dataSource, onChangeVersion, deprecated, specProtocol }: IProps) => {
  const params = routeInfoStore.useStore((s) => s.params);
  const [hasAccess, accessDetail] = apiMarketStore.useStore((s) => [
    s.assetVersionDetail.hasAccess,
    s.assetVersionDetail.access,
  ]);
  const [{ apiData, currentApi, testModalVisible, authModal, authed }, updater, update] = useUpdate<IState>({
    apiData: {},
    currentApi: '',
    testModalVisible: false,
    authModal: false,
    authed: false,
  });
  React.useEffect(() => {
    if (dataSource.spec) {
      SwaggerParser.dereference(
        cloneDeep(dataSource.spec),
        {},
        (err: Error | null, data: OpenAPI.Document | undefined) => {
          if (err) {
            message.error(i18n.t('default:failed to parse API description document'));
            throw err;
          }
          updater.apiData((data || {}) as ApiData);
        },
      );
    }
    return () => {
      updater.apiData({} as ApiData);
    };
  }, [dataSource.spec, updater]);
  const getAuthInfo = React.useCallback(() => {
    const authInfo = JSON.parse(sessionStorage.getItem(`asset-${params.assetID}`) || '{}');
    return authInfo[`version-${params.versionID}`];
  }, [params.assetID, params.versionID]);
  React.useEffect(() => {
    const authInfo = getAuthInfo();
    let isAuthed = !isEmpty(authInfo);
    if (isAuthed && accessDetail.authentication === authenticationMap['sign-auth'].value) {
      isAuthed = !!authInfo.clientSecret;
    }
    updater.authed(isAuthed);
  }, [getAuthInfo, updater, accessDetail.authentication]);
  React.useEffect(() => {
    return () => {
      sessionStorage.removeItem(`asset-${params.assetID}`);
    };
  }, [params.assetID]);
  const fullingUrl = React.useCallback(
    (path: string) => {
      let url = path;
      if (apiData.basePath && !['', '/'].includes(apiData.basePath)) {
        url = apiData.basePath + path;
      }
      return url;
    },
    [apiData.basePath],
  );
  const [tagMap, apiMap] = React.useMemo(() => {
    const _tagMap = {} as TagMap;
    const _apiMap = {} as { [K: string]: ApiMapItem };
    if (isEmpty(apiData.paths)) {
      return [{}, {}];
    }
    map(apiData.paths, (methodMap, path) => {
      const _path = fullingUrl(path);
      const httpRequests = pick(methodMap, HTTP_METHODS);
      const restParams = omit(methodMap, HTTP_METHODS);
      map(httpRequests, (api, method) => {
        const parameters = uniqWith(
          [...(api.parameters || [])].concat(restParams.parameters || []),
          (a, b) => a.in === b.in && a.name === b.name,
        );
        const item: ApiMapItem = {
          _method: method,
          _path,
          ...api,
          ...restParams,
          parameters,
        };
        map(api.tags || ['OTHER'], (tagName) => {
          if (_tagMap[tagName]) {
            _tagMap[tagName].push(item);
          } else {
            _tagMap[tagName] = [];
            _tagMap[tagName].push(item);
          }
        });
        _apiMap[method + _path] = item;
      });
    });
    return [_tagMap, _apiMap];
  }, [apiData.paths, fullingUrl]);

  const handleChange = React.useCallback(
    (key: string) => {
      updater.currentApi(key);
    },
    [updater],
  );
  const handleShowTest = () => {
    if (!authed) {
      message.error(i18n.t('please authenticate first'));
      return;
    }
    updater.testModalVisible(true);
  };
  const handleOk = (data: AutoInfo) => {
    const authInfo = JSON.parse(sessionStorage.getItem(`asset-${params.assetID}`) || '{}');
    authInfo[`version-${params.versionID}`] = data;
    sessionStorage.setItem(`asset-${params.assetID}`, JSON.stringify(authInfo));
    update({
      authed: true,
      authModal: false,
    });
  };

  const testButton = hasAccess ? (
    <>
      <Button onClick={handleShowTest}>{i18n.t('test')}</Button>
      {
        <Button
          className="ml-2"
          onClick={() => {
            updater.authModal(true);
          }}
        >
          {authed ? i18n.t('recertification') : i18n.t('authentication')}
        </Button>
      }
    </>
  ) : null;
  const fieldsList: IFormItem[] = [
    {
      label: 'clientID',
      name: 'clientID',
    },
    ...insertWhen(accessDetail.authentication === authenticationMap['sign-auth'].value, [
      {
        label: 'clientSecret',
        name: 'clientSecret',
        getComp: () => <Input.Password />,
      },
    ]),
  ];
  const currentApiSource = apiMap[currentApi] || {};
  const parametersMap: Dictionary<any[]> = groupBy(currentApiSource.parameters, 'in');
  if (specProtocol && specProtocol.includes('oas3')) {
    Object.assign(parametersMap, convertToOpenApi2(currentApiSource));
  }
  const autoInfo = getAuthInfo();
  return (
    <div className="apis-view flex justify-between items-center flex-1">
      <div className="apis-view-left">
        <ApiMenu list={tagMap} onChange={handleChange} onChangeVersion={onChangeVersion} />
      </div>
      <div className="apis-view-right">
        {deprecated ? (
          <ErdaAlert className="mb-4" type="warning" message={i18n.t('the current version is deprecated')} />
        ) : null}
        <ApiDetail key={currentApi} dataSource={currentApiSource} extra={testButton} specProtocol={specProtocol} />
      </div>
      <TestModal
        key={`${currentApiSource._method}${currentApiSource._path}`}
        visible={testModalVisible}
        onCancel={() => {
          updater.testModalVisible(false);
        }}
        dataSource={{
          autoInfo,
          basePath: apiData.basePath,
          url: currentApiSource._path,
          method: currentApiSource._method?.toUpperCase(),
          requestScheme: parametersMap,
          host: apiData.host,
          protocol: apiData.schemes?.includes('https') ? 'https' : 'http',
        }}
      />
      <FormModal
        title={i18n.t('authentication')}
        visible={authModal}
        fieldsList={fieldsList}
        onCancel={() => {
          updater.authModal(false);
        }}
        formData={getAuthInfo()}
        onOk={handleOk}
      />
    </div>
  );
}
Example #29
Source File: api-preview-2.0.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiPreviewV2 = (props: IProps) => {
  const { dataSource, extra } = props;
  // v2.0版本in的枚举值: query、header、path、formData、body
  const parametersMap: Dictionary<any[]> = groupBy(dataSource.parameters, 'in');
  const bodyParams = getBodyParams(get(parametersMap, 'body[0]'));
  const bodyParamsPureType = get(parametersMap, 'body[0].schema.type');
  const { responseCode, responseData, responseSchemaType } = getResponseBody(dataSource);
  const previewData = {
    data: {
      info: {
        title: dataSource.summary || dataSource.description || dataSource._path,
        desc: dataSource.description,
        apiInfo: { method: dataSource._method, path: dataSource._path },
        urlParams: parametersMap.query,
        pathParams: parametersMap.path,
        bodyParams,
        formData: parametersMap.formData,
        headerParams: parametersMap.header,
        responseCode,
        responseData,
      },
    },
    props: {
      render: [
        { type: 'Title', dataIndex: 'title', props: { titleExtra: extra } },
        { type: 'Desc', dataIndex: 'desc' },
        { type: 'BlockTitle', props: { title: i18n.t('request information') } },
        { type: 'API', dataIndex: 'apiInfo' },
        ...insertWhen(!!parametersMap.query, [
          {
            type: 'Table',
            dataIndex: 'urlParams',
            props: {
              title: i18n.t('URL parameters'),
              rowKey: 'name',
              columns: [...columns, { title: i18n.t('Default value'), dataIndex: 'default' }],
            },
          },
        ]),
        ...insertWhen(!!parametersMap.path, [
          {
            type: 'Table',
            dataIndex: 'pathParams',
            props: {
              title: i18n.t('path parameters'),
              rowKey: 'name',
              columns: [...columns, { title: i18n.t('Default value'), dataIndex: 'default' }],
            },
          },
        ]),
        !isEmpty(bodyParams)
          ? {
              type: 'Table',
              dataIndex: 'bodyParams',
              props: {
                title: i18n.t('request body'),
                rowKey: 'key',
                columns,
              },
            }
          : bodyParamsPureType
          ? {
              type: 'Title',
              props: { title: `${i18n.t('request body')}: ${bodyParamsPureType}`, level: 2 },
            }
          : null,
        !isEmpty(parametersMap.formData)
          ? {
              type: 'Table',
              dataIndex: 'formData',
              props: {
                title: i18n.t('request body'),
                rowKey: 'key',
                columns,
              },
            }
          : null,
        ...insertWhen(!!parametersMap.header, [
          {
            type: 'Table',
            dataIndex: 'headerParams',
            props: {
              title: i18n.t('request header'),
              rowKey: 'name',
              columns: [...columns, { title: i18n.t('Default value'), dataIndex: 'default' }],
            },
          },
        ]),
        { type: 'BlockTitle', props: { title: i18n.t('response information') } },
        isEmpty(responseData)
          ? {
              type: 'Title',
              props: { title: `${i18n.t('Response body')}: ${responseSchemaType || i18n.t('None')} `, level: 2 },
            }
          : {
              type: 'Table',
              dataIndex: 'responseData',
              props: {
                title: `${i18n.t('Response body')}${responseSchemaType ? `: ${responseSchemaType}` : ''} `,
                rowKey: 'key',
                columns,
              },
            },
        isEmpty(responseCode)
          ? {
              type: 'Title',
              props: { title: `${i18n.t('response code')}: ${i18n.t('None')}`, level: 2 },
            }
          : {
              type: 'Table',
              dataIndex: 'responseCode',
              props: {
                title: i18n.t('response code'),
                rowKey: 'code',
                columns: [
                  { title: i18n.t('response code'), dataIndex: 'code' },
                  { title: i18n.t('Description'), dataIndex: 'desc' },
                ],
              },
            },
      ],
    },
  } as Obj as CP_INFO_PREVIEW.Props;
  return <InfoPreview {...previewData} />;
}